diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6fe68cc8e93..ca29fe71fba 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -6,7 +6,7 @@ pnpm 10 + Turborepo monorepo. Requires Node >= 22. | Directory | Purpose | | ----------------- | ------------------------------------------------------------ | -| `apps/studio` | Supabase Studio/Dashboard — Next.js (pages router), React 18 | +| `apps/studio` | Supabase Studio/Dashboard — Next.js (pages router), React 19 | | `apps/docs` | Documentation site | | `apps/www` | Marketing website | | `packages/ui` | Shared UI components (shadcn/ui based) | diff --git a/.claude/skills/safe-sql-execution/SKILL.md b/.claude/skills/safe-sql-execution/SKILL.md index 6cf59379d04..c0f43cb8f8f 100644 --- a/.claude/skills/safe-sql-execution/SKILL.md +++ b/.claude/skills/safe-sql-execution/SKILL.md @@ -279,7 +279,7 @@ function MyBadComponent() { ### Round-tripping SQL from the database (NOT snippet content) ```ts -// ✅ GOOD: SQL from the database is promoted to SafeSqlFragment at the point +// ✅ GOOD: SQL from the database is promoted to SafeSqlFragment at the point // of fetching // data/function-definitions.ts @@ -323,10 +323,10 @@ function MyComponent() { ### Snippet content is ALWAYS UNSAFE -Snippets are auto-persisted to the database and can be created or modified -through externally influenceable channels (e.g., prefilled from URL params). -The `unchecked_sql` property is typed as `UntrustedSqlFragment` to enforce this -— it must only be promoted to `SafeSqlFragment` via `acceptUntrustedSql` in an +Snippets are auto-persisted to the database and can be created or modified +through externally influenceable channels (e.g., prefilled from URL params). +The `unchecked_sql` property is typed as `UntrustedSqlFragment` to enforce this +— it must only be promoted to `SafeSqlFragment` via `acceptUntrustedSql` in an event handler that requires explicit user action. ```ts @@ -376,3 +376,65 @@ function SnippetRunner({ snippet }: { snippet: Snippet }) { ) } ``` + +## Analytics SQL (BigQuery / ClickHouse) + +The same security model applies to analytics queries, which target BigQuery +or ClickHouse via the +`/platform/projects/{ref}/analytics/endpoints/logs.all{,.otel}` endpoints. +Filter keys and values from URL parameters and UI inputs are spliced into SQL +that runs against the project's logs, so the same injection risk exists. + +The brand and helpers live in `apps/studio/data/logs/safe-analytics-sql.ts`, +intentionally **disjoint** from the pg-meta `SafeSqlFragment` brand: + +- `SafeLogSqlFragment` — branded type for analytics SQL. +- `safeSql` — template tag that only accepts `SafeLogSqlFragment` + interpolations. +- `analyticsLiteral(value)` — sanitizes string/number/boolean literals. +- `quotedIdent(name)` — validates and backtick-quotes dotted identifiers. +- `keyword(value, allowed)` — validates against an allow-list of operators. +- `joinSqlFragments(fragments, separator)` — composes already-branded + fragments. + +The brands are kept separate because escape semantics differ — Postgres-safe +`E'…'` strings, `::jsonb` casts, and double-quoted identifiers are unsafe for +BigQuery and/or ClickHouse, and vice versa. Crossing the brands would silently +emit unsafe SQL. + +The wire-boundary wrapper is `executeAnalyticsSql` in +`apps/studio/data/logs/execute-analytics-sql.ts`, analogous to pg-meta's +`executeSql`. It accepts only `SafeLogSqlFragment` for its `sql` parameter, so +raw strings are rejected at compile time. A grep-based vitest +(`apps/studio/tests/unit/lints/analytics-sql-boundary.test.ts`) prevents +regressions by failing the build if any file outside +`execute-analytics-sql.ts` calls `post()` or `get()` directly against +`logs.all` or `logs.all.otel`. + +```ts +import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql' +import { analyticsLiteral, quotedIdent, safeSql } from '@/data/logs/safe-analytics-sql' + +// ✅ GOOD: every interpolation is sanitized. +const sql = safeSql` + SELECT timestamp, event_message + FROM ${quotedIdent(table)} + WHERE id = ${analyticsLiteral(id)} +` + +await executeAnalyticsSql({ + projectRef, + endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all', + sql, + iso_timestamp_start, + iso_timestamp_end, +}) +``` + +```ts +// 🛑 BAD: raw string interpolation. This fails to type-check at the +// executeAnalyticsSql boundary because the result is `string`, not +// `SafeLogSqlFragment`. +const sql = `SELECT * FROM ${table} WHERE id = '${id}'` +await executeAnalyticsSql({ projectRef, endpoint, sql, ... }) +``` diff --git a/.claude/skills/studio-mock-api-tests/SKILL.md b/.claude/skills/studio-mock-api-tests/SKILL.md new file mode 100644 index 00000000000..f073e305750 --- /dev/null +++ b/.claude/skills/studio-mock-api-tests/SKILL.md @@ -0,0 +1,294 @@ +--- +name: studio-mock-api-tests +description: Component tests for Supabase Studio that mock API requests at the + network layer with MSW. Use when writing or reviewing a component test that + exercises a React Query hook or mutation, or when migrating an existing + test away from vi.mock('@/data/...'). Covers the customRender + addAPIMock + template and the jsdom/MSW gotchas that cost real debugging time. +--- + +# Studio MSW component tests + +Mount a Studio component, intercept its network calls with MSW, assert +what renders and what gets sent. The infrastructure is already wired up — +this skill is the working template plus the gotchas. + +## When to use + +- The component (or any descendant it renders) calls a React Query hook + or mutation that hits `/platform/...`, `/v1/...`, or another endpoint + in `apps/studio/data/api.d.ts`. +- You'd otherwise be tempted to write `vi.mock('@/data/some-query', ...)`. + **Don't.** Mock the network instead — see "Why not vi.mock" below. + +If the component is purely presentational with no data fetching, you +don't need MSW; render and assert directly. + +## The template + +```tsx +import { fireEvent, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { HttpResponse } from 'msw' +import { describe, expect, test, vi } from 'vitest' + +import { MyComponent } from './MyComponent' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +// Needed if the component renders inside a Sheet, Modal, Popover, or +// anything else built on Radix that uses Web Animations. +mockAnimationsApi() + +describe('MyComponent', () => { + test('renders rows from the API', async () => { + addAPIMock({ + method: 'get', + path: '/platform/organizations', + response: () => + HttpResponse.json([ + { + /* ... */ + }, + ]), + }) + + customRender() + + expect(await screen.findByText('Acme')).toBeInTheDocument() + }) +}) +``` + +That's the whole pattern. Server lifecycle (`listen`/`resetHandlers`/ +`close`) is handled by `apps/studio/tests/vitestSetup.ts` — handlers +registered via `addAPIMock` are scoped to the current test. + +## Gotchas that will eat your afternoon + +### 1. Path params use `:slug`, not `{slug}` + +`addAPIMock` is typed from the OpenAPI `paths`, but path params are +remapped to MSW's `:param` format. Autocomplete will guide you, but if +typecheck reports the path isn't assignable, you're using the OpenAPI +`{slug}` form. + +```ts +// ❌ TypeScript error, MSW won't match +path: '/platform/organizations/{slug}/projects' + +// ✅ Correct +path: '/platform/organizations/:slug/projects' +``` + +### 2. Use `HttpResponse.json`, not `new HttpResponse` + +For success responses, always go through `HttpResponse.json` — even for +204/201-no-content endpoints. A raw `new HttpResponse(null, { status: 201 })` +returns no content-type, and `openapi-fetch` can hang the mutation flow, +which silently breaks `onSuccess` callbacks. + +```ts +// ❌ Mutation onSuccess silently never fires +response: () => new HttpResponse(null, { status: 201 }) + +// ✅ Works (pass the OpenAPI body shape explicitly — see gotcha #8) +response: () => HttpResponse.json({}, { status: 201 }) +``` + +### 3. Submit buttons in Sheets/Modals need `fireEvent.click` + +The convention ` diff --git a/apps/design-system/components/command-menu.tsx b/apps/design-system/components/command-menu.tsx index 2fa3cfee17f..89886d4a59e 100644 --- a/apps/design-system/components/command-menu.tsx +++ b/apps/design-system/components/command-menu.tsx @@ -54,7 +54,7 @@ export function CommandMenu({ ...props }: DialogProps) { return ( <> + ``` -_It is likely that the `type` prop will be changed to `variant` in the future._ - ## Link You can use the `buttonVariants` helper to create a link that looks like a button. @@ -47,9 +45,9 @@ Use the `size` prop to determine the size of the button. -### Types +### Variants -These are all the different `type` variations. +These are all the different `variant` variations. #### Primary @@ -61,15 +59,15 @@ Used for data insertion actions, confirming purchases, strong positive actions. Used for opening dialogs, navigating to pages, and other non CRUD actions. -This `type` will probably be the most used button type. -It will probably be changed to be the default type in future. +This `variant` will probably be the most used button variant. +It will probably be changed to be the default variant in future. #### Secondary Can be used for signaling a data or config change, but not as serious as a primary button. -For destructive or side effect actions, use the `destructive` or `warning` type. +For destructive or side effect actions, use the `destructive` or `warning` variant. @@ -83,7 +81,7 @@ Used for actions that might have a side effect, but not as serious as a destruct Used for actions that will have a serious destructive side effect, like deleting data. -prop `type` will probably be changed to `destructive` in the future. +prop `variant` will probably be changed to `destructive` in the future. @@ -97,7 +95,7 @@ Used for secondary actions, or actions that are not as important as the primary Used for actions that are not as important as the primary action, or for actions that are not as important as the primary action. -prop `type` will probably be changed to `ghost` in the future. +prop `variant` will probably be changed to `ghost` in the future. diff --git a/apps/design-system/content/docs/components/date-picker.mdx b/apps/design-system/content/docs/components/date-picker.mdx index 43040dff22c..286c3338eed 100644 --- a/apps/design-system/content/docs/components/date-picker.mdx +++ b/apps/design-system/content/docs/components/date-picker.mdx @@ -10,7 +10,7 @@ source: ## Installation -The Date Picker is built using a composition of the `` and the `` components. +The Date Picker is built using a composition of the ``, ` - - + + + - - + + ) } ``` See the [React DayPicker](https://react-day-picker.js.org) documentation for more information. +## API Reference + +- `` accepts the same props as [the `` component](./popover). +- `` accepts the same props as [the `` component](./popover#api-reference). +- `` accepts the same props as [the `` component](./popover#api-reference). +- `` accepts the same props as [the ` } diff --git a/apps/design-system/content/docs/fragments/page-breadcrumbs.mdx b/apps/design-system/content/docs/fragments/page-breadcrumbs.mdx index 9b995f4a5ad..40375274ee3 100644 --- a/apps/design-system/content/docs/fragments/page-breadcrumbs.mdx +++ b/apps/design-system/content/docs/fragments/page-breadcrumbs.mdx @@ -24,7 +24,7 @@ import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcr - diff --git a/apps/design-system/content/docs/fragments/page-header.mdx b/apps/design-system/content/docs/fragments/page-header.mdx index dd754709723..05fa7a1d1f8 100644 --- a/apps/design-system/content/docs/fragments/page-header.mdx +++ b/apps/design-system/content/docs/fragments/page-header.mdx @@ -44,7 +44,7 @@ Compound header for page context: optional icon, title, description, and aside a Manage email templates for your project. - diff --git a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx index 12f90b69c7a..3dc9d17280a 100644 --- a/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx +++ b/apps/design-system/content/docs/fragments/text-confirm-dialog.mdx @@ -29,7 +29,7 @@ export default function TextConfirmDialogDemo() { return ( <> - diff --git a/apps/design-system/content/docs/ui-patterns/introduction.mdx b/apps/design-system/content/docs/ui-patterns/introduction.mdx index b0fe4b736dc..59dc6599381 100644 --- a/apps/design-system/content/docs/ui-patterns/introduction.mdx +++ b/apps/design-system/content/docs/ui-patterns/introduction.mdx @@ -11,6 +11,7 @@ These patterns help ensure consistency across Supabase products by establishing - **[Empty States](empty-states)**: Communicating the absence of data and guiding users toward meaningful actions. - **[Forms](forms)**: Building cohesive form experiences in both page layouts and side panels. - **[Layout](layout)**: Creating consistent page structures with proper spacing, max-widths, and content organization. +- **[Markdown](markdown)**: Rendering composable markdown content with defaults - **[Navigation](navigation)**: Organizing complex hierarchical navigation systems across multiple products and contexts. UI patterns may incorporate external libraries (such as `react-markdown`, `reactflow`, `recharts`, etc) or compose various components from the `ui` package. They serve as blueprints for solving recurring design problems, ensuring that similar features across the application follow the same structural and interaction patterns. diff --git a/apps/design-system/content/docs/ui-patterns/markdown.mdx b/apps/design-system/content/docs/ui-patterns/markdown.mdx new file mode 100644 index 00000000000..ac69e60d506 --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/markdown.mdx @@ -0,0 +1,101 @@ +--- +title: Markdown +description: Composable Markdown renderer +--- + +A composable `react-markdown` component with defaults for all standard markdown elements (headings, paragraphs, lists, blockquotes, links, images, tables, code). Customize any element via the `components` prop and import optional components such as Quote or Avatar. +All components are lazy-loaded. + + + +## API + +| Prop | Type | Default | Description | +| --------------- | --------------------- | ------------- | ---------------------------------------------------------------------- | +| `children` | `string` | - | Markdown content to render | +| `content` | `string` | `''` | **Deprecated**: Use `children` instead | +| `codeBlock` | `boolean` | `false` | Enable syntax highlighting for code blocks (lazy-loaded via CodeBlock) | +| `components` | `Partial` | - | Override or add specific markdown elements | +| `className` | `string` | - | CSS class for the wrapper div | +| `remarkPlugins` | `PluggableList` | `[remarkGfm]` | Additional remark plugins (GFM is included by default) | + +All other `react-markdown` options are supported via spread props. + +## Customization + +Override any element or add new ones via the `components` prop. + + + +## Primitive Components + +```tsx +import { + Anchor, + Avatar, + Blockquote, + Code, + CodeBlockPre, + DefaultPre, + Hr, + Img, + Paragraph, + Quote, + SimplePre, + Table, + Td, + Th, + Tr, + UnorderedList, +} from 'ui-patterns/Markdown' +``` + +### Headings + + + +### Paragraphs + + + +### Lists + + + +### Links + + + +### Inline Code + + + +### Blockquotes + + + +### Code Blocks + + + +### Tables + + + +### Images + + + +### Horizontal Rules + + + +## Optional Components + +### Quote + + + +### Avatar + + diff --git a/apps/design-system/content/docs/ui-patterns/modality.mdx b/apps/design-system/content/docs/ui-patterns/modality.mdx index ddb14d08fd0..adb2b0fc4e5 100644 --- a/apps/design-system/content/docs/ui-patterns/modality.mdx +++ b/apps/design-system/content/docs/ui-patterns/modality.mdx @@ -116,7 +116,7 @@ const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ ... - ... diff --git a/apps/design-system/lib/remark-jsx-components.ts b/apps/design-system/lib/remark-jsx-components.ts new file mode 100644 index 00000000000..d54e3c5e019 --- /dev/null +++ b/apps/design-system/lib/remark-jsx-components.ts @@ -0,0 +1,33 @@ +import { visit } from 'unist-util-visit' + +// Matches self-closing JSX-like elements with PascalCase component names: +// +// +const JSX_SELF_CLOSING = /^<([A-Z]\w*)((?:\s+[\w-]+(?:="[^"]*")?)*)\s*\/>$/ +const ATTR_PATTERN = /([\w-]+)(?:="([^"]*)")?/g + +/** + * Remark plugin that converts JSX-like self-closing tags (e.g. ``) + * in markdown into nodes that react-markdown maps to entries in the `components` prop — + * giving the ui-patterns `Markdown` component MDX-like behavior in design-system demos. + */ +export const remarkJsxComponents = () => (tree: any) => { + visit(tree, 'html', (node: any, index: number | undefined, parent: any) => { + if (!parent || index === undefined) return + const match = node.value.trim().match(JSX_SELF_CLOSING) + if (!match) return + + const [, name, attrsStr] = match + const properties: Record = {} + ATTR_PATTERN.lastIndex = 0 + let attrMatch: RegExpExecArray | null + while ((attrMatch = ATTR_PATTERN.exec(attrsStr || '')) !== null) { + properties[attrMatch[1]] = attrMatch[2] !== undefined ? attrMatch[2] : true + } + + parent.children[index] = { + type: 'jsxComponent', + data: { hName: name, hProperties: properties }, + } + }) +} diff --git a/apps/design-system/registry/default/block/chart-composed-states.tsx b/apps/design-system/registry/default/block/chart-composed-states.tsx index c4e25d545c2..2d3657cca2c 100644 --- a/apps/design-system/registry/default/block/chart-composed-states.tsx +++ b/apps/design-system/registry/default/block/chart-composed-states.tsx @@ -1,7 +1,7 @@ 'use client' import { BarChart2, ExternalLink } from 'lucide-react' -import { Badge } from 'ui' +import { Badge, CriticalIcon } from 'ui' import { Chart, ChartActions, @@ -44,6 +44,35 @@ export default function ChartComposedStates() { + + + + Response Errors + + + } + title="Some error happened" + description="Error happened why trying to fetch data. Please try again later." + /> + } + emptyState={ + } + title="No data to show" + description="It may take up to 24 hours for data to refresh" + /> + } + loadingState={} + > + My chart here... + + + + diff --git a/apps/design-system/registry/default/example/admonition-button.tsx b/apps/design-system/registry/default/example/admonition-button.tsx index b6251de2bef..2296942ff60 100644 --- a/apps/design-system/registry/default/example/admonition-button.tsx +++ b/apps/design-system/registry/default/example/admonition-button.tsx @@ -10,14 +10,14 @@ export default function AdmonitionDemo() { title="Set up custom SMTP" description="You’re using the built-in email service. This service has rate limits and is not meant to be used for production apps." - actions={} + actions={} /> Delete organization} + actions={} /> ) diff --git a/apps/design-system/registry/default/example/admonition-demo.tsx b/apps/design-system/registry/default/example/admonition-demo.tsx index 75040b9fc63..78e8a2300ff 100644 --- a/apps/design-system/registry/default/example/admonition-demo.tsx +++ b/apps/design-system/registry/default/example/admonition-demo.tsx @@ -9,7 +9,7 @@ export default function AdmonitionDemo() { title="OAuth Server is disabled" description="Enable OAuth Server to make your project act as an identity provider for third-party applications." - actions={} + actions={} /> ) } diff --git a/apps/design-system/registry/default/example/admonition-responsive.tsx b/apps/design-system/registry/default/example/admonition-responsive.tsx index 5ad40cdcc72..30b2d481ff8 100644 --- a/apps/design-system/registry/default/example/admonition-responsive.tsx +++ b/apps/design-system/registry/default/example/admonition-responsive.tsx @@ -8,7 +8,7 @@ export default function AdmonitionDemo() { layout="responsive" title="Disk management has moved" description="Disk management is now handled alongside Project Compute on the Compute and Disk page." - actions={} + actions={} /> ) } diff --git a/apps/design-system/registry/default/example/alert-dialog-async-error.tsx b/apps/design-system/registry/default/example/alert-dialog-async-error.tsx index 21cd9c9d7c1..188a256861a 100644 --- a/apps/design-system/registry/default/example/alert-dialog-async-error.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-async-error.tsx @@ -36,7 +36,7 @@ export default function AlertDialogAsyncError() { return ( - + diff --git a/apps/design-system/registry/default/example/alert-dialog-async.tsx b/apps/design-system/registry/default/example/alert-dialog-async.tsx index a34c37fb463..47749ce1b84 100644 --- a/apps/design-system/registry/default/example/alert-dialog-async.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-async.tsx @@ -19,7 +19,7 @@ export default function AlertDialogAsync() { return ( - + diff --git a/apps/design-system/registry/default/example/alert-dialog-close-only.tsx b/apps/design-system/registry/default/example/alert-dialog-close-only.tsx index 204d3e3b970..975ae516372 100644 --- a/apps/design-system/registry/default/example/alert-dialog-close-only.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-close-only.tsx @@ -14,7 +14,7 @@ export default function AlertDialogCloseOnly() { return ( - + diff --git a/apps/design-system/registry/default/example/alert-dialog-demo.tsx b/apps/design-system/registry/default/example/alert-dialog-demo.tsx index 447be8975eb..d38011820e2 100644 --- a/apps/design-system/registry/default/example/alert-dialog-demo.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-demo.tsx @@ -15,7 +15,7 @@ export default function AlertDialogDemo() { return ( - + diff --git a/apps/design-system/registry/default/example/alert-dialog-destructive.tsx b/apps/design-system/registry/default/example/alert-dialog-destructive.tsx index 6edea2532a1..62446169204 100644 --- a/apps/design-system/registry/default/example/alert-dialog-destructive.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-destructive.tsx @@ -15,7 +15,7 @@ export default function AlertDialogDestructive() { return ( - + diff --git a/apps/design-system/registry/default/example/alert-dialog-warning.tsx b/apps/design-system/registry/default/example/alert-dialog-warning.tsx index 8b95feaa82a..82bd2d8c182 100644 --- a/apps/design-system/registry/default/example/alert-dialog-warning.tsx +++ b/apps/design-system/registry/default/example/alert-dialog-warning.tsx @@ -15,7 +15,7 @@ export default function AlertDialogWarning() { return ( - + diff --git a/apps/design-system/registry/default/example/button-default.tsx b/apps/design-system/registry/default/example/button-default.tsx index 33b22371e51..1a3c0024e20 100644 --- a/apps/design-system/registry/default/example/button-default.tsx +++ b/apps/design-system/registry/default/example/button-default.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonDemo() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-demo.tsx b/apps/design-system/registry/default/example/button-demo.tsx index be612ebfafa..50e28157d2a 100644 --- a/apps/design-system/registry/default/example/button-demo.tsx +++ b/apps/design-system/registry/default/example/button-demo.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonDemo() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-destructive.tsx b/apps/design-system/registry/default/example/button-destructive.tsx index 33e1c7005ec..7eb3d779f5a 100644 --- a/apps/design-system/registry/default/example/button-destructive.tsx +++ b/apps/design-system/registry/default/example/button-destructive.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonDestructive() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-ghost.tsx b/apps/design-system/registry/default/example/button-ghost.tsx index 0230b5a104b..3fe558ea80b 100644 --- a/apps/design-system/registry/default/example/button-ghost.tsx +++ b/apps/design-system/registry/default/example/button-ghost.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonGhost() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-icon.tsx b/apps/design-system/registry/default/example/button-icon.tsx index f140520b763..e9cac7c1660 100644 --- a/apps/design-system/registry/default/example/button-icon.tsx +++ b/apps/design-system/registry/default/example/button-icon.tsx @@ -2,5 +2,5 @@ import { ChevronRight } from 'lucide-react' import { Button } from 'ui' export default function ButtonIcon() { - return + return } diff --git a/apps/design-system/registry/default/example/button-link.tsx b/apps/design-system/registry/default/example/button-link.tsx index a34a15aa152..8710648d0b3 100644 --- a/apps/design-system/registry/default/example/button-link.tsx +++ b/apps/design-system/registry/default/example/button-link.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonLink() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-outline.tsx b/apps/design-system/registry/default/example/button-outline.tsx index da8039f0c4d..ce2f599701a 100644 --- a/apps/design-system/registry/default/example/button-outline.tsx +++ b/apps/design-system/registry/default/example/button-outline.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonOutline() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-secondary.tsx b/apps/design-system/registry/default/example/button-secondary.tsx index 59a1fdcdcaf..71f6cdc7e30 100644 --- a/apps/design-system/registry/default/example/button-secondary.tsx +++ b/apps/design-system/registry/default/example/button-secondary.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonSecondary() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/button-sizes.tsx b/apps/design-system/registry/default/example/button-sizes.tsx index d625a4e6305..9dbd9b642d2 100644 --- a/apps/design-system/registry/default/example/button-sizes.tsx +++ b/apps/design-system/registry/default/example/button-sizes.tsx @@ -4,19 +4,19 @@ import { Button } from 'ui' export default function ButtonDemo() { return (
- - - - -
diff --git a/apps/design-system/registry/default/example/button-warning.tsx b/apps/design-system/registry/default/example/button-warning.tsx index e50b6abc8c7..9257d0e2980 100644 --- a/apps/design-system/registry/default/example/button-warning.tsx +++ b/apps/design-system/registry/default/example/button-warning.tsx @@ -4,14 +4,14 @@ import { Button } from 'ui' export default function ButtonDemo() { return (
- - + - -
diff --git a/apps/design-system/registry/default/example/calendar-form.tsx b/apps/design-system/registry/default/example/calendar-form.tsx index c94a7f56050..99846d6f4b8 100644 --- a/apps/design-system/registry/default/example/calendar-form.tsx +++ b/apps/design-system/registry/default/example/calendar-form.tsx @@ -57,7 +57,7 @@ export default function CalendarForm() { + ) diff --git a/apps/design-system/registry/default/example/calendar-react-hook-form.tsx b/apps/design-system/registry/default/example/calendar-react-hook-form.tsx index fd580854922..7cc4683d4ed 100644 --- a/apps/design-system/registry/default/example/calendar-react-hook-form.tsx +++ b/apps/design-system/registry/default/example/calendar-react-hook-form.tsx @@ -57,7 +57,7 @@ export default function CalendarForm() { + ) diff --git a/apps/design-system/registry/default/example/checkbox-form-multiple.tsx b/apps/design-system/registry/default/example/checkbox-form-multiple.tsx index bbf536fedad..006ad6bad2a 100644 --- a/apps/design-system/registry/default/example/checkbox-form-multiple.tsx +++ b/apps/design-system/registry/default/example/checkbox-form-multiple.tsx @@ -112,7 +112,7 @@ export default function CheckboxReactHookFormMultiple() { )} /> - + ) diff --git a/apps/design-system/registry/default/example/checkbox-form-single.tsx b/apps/design-system/registry/default/example/checkbox-form-single.tsx index 8a9dad9fffd..f18d4a172ef 100644 --- a/apps/design-system/registry/default/example/checkbox-form-single.tsx +++ b/apps/design-system/registry/default/example/checkbox-form-single.tsx @@ -59,7 +59,7 @@ export default function CheckboxReactHookFormSingle() { )} /> - + ) diff --git a/apps/design-system/registry/default/example/collapsible-demo.tsx b/apps/design-system/registry/default/example/collapsible-demo.tsx index e989e8c154e..e9bf6e8d0c3 100644 --- a/apps/design-system/registry/default/example/collapsible-demo.tsx +++ b/apps/design-system/registry/default/example/collapsible-demo.tsx @@ -12,7 +12,7 @@ export default function CollapsibleDemo() {

@peduarte starred 3 repositories

- diff --git a/apps/design-system/registry/default/example/combobox-demo.tsx b/apps/design-system/registry/default/example/combobox-demo.tsx index b7f8fabd978..5216f8b4ada 100644 --- a/apps/design-system/registry/default/example/combobox-demo.tsx +++ b/apps/design-system/registry/default/example/combobox-demo.tsx @@ -48,7 +48,7 @@ export default function ComboboxDemo() { diff --git a/apps/design-system/registry/default/example/combobox-form.tsx b/apps/design-system/registry/default/example/combobox-form.tsx index b6cd2f507d1..a0a71ccd955 100644 --- a/apps/design-system/registry/default/example/combobox-form.tsx +++ b/apps/design-system/registry/default/example/combobox-form.tsx @@ -73,7 +73,7 @@ export default function ComboboxForm() { diff --git a/apps/design-system/registry/default/example/combobox-popover.tsx b/apps/design-system/registry/default/example/combobox-popover.tsx index 4c2df6b951c..3ddb32386cc 100644 --- a/apps/design-system/registry/default/example/combobox-popover.tsx +++ b/apps/design-system/registry/default/example/combobox-popover.tsx @@ -69,7 +69,7 @@ export default function ComboboxPopover() { diff --git a/apps/design-system/registry/default/example/confirmation-modal-demo.tsx b/apps/design-system/registry/default/example/confirmation-modal-demo.tsx index 49268e20a35..140ef286421 100644 --- a/apps/design-system/registry/default/example/confirmation-modal-demo.tsx +++ b/apps/design-system/registry/default/example/confirmation-modal-demo.tsx @@ -24,7 +24,7 @@ export default function ConfirmationModalDemo() { return ( <> -
Bad Example - - - + + +
Good Example - - - + + +
) diff --git a/apps/design-system/registry/default/example/copy-confirmations.tsx b/apps/design-system/registry/default/example/copy-confirmations.tsx index a60d869ab1b..d99d184b00c 100644 --- a/apps/design-system/registry/default/example/copy-confirmations.tsx +++ b/apps/design-system/registry/default/example/copy-confirmations.tsx @@ -18,10 +18,10 @@ export default function CopyConfirmations() {
- -
@@ -40,10 +40,10 @@ export default function CopyConfirmations() {
- -
diff --git a/apps/design-system/registry/default/example/copy-empty-states.tsx b/apps/design-system/registry/default/example/copy-empty-states.tsx index cf0415f5c28..7008b7ef0cf 100644 --- a/apps/design-system/registry/default/example/copy-empty-states.tsx +++ b/apps/design-system/registry/default/example/copy-empty-states.tsx @@ -25,7 +25,7 @@ export default function CopyEmptyStates() { title="No API keys" description="Generate a key to connect your application." > - + diff --git a/apps/design-system/registry/default/example/copy-loading-states.tsx b/apps/design-system/registry/default/example/copy-loading-states.tsx index db1259dc196..54c053276d5 100644 --- a/apps/design-system/registry/default/example/copy-loading-states.tsx +++ b/apps/design-system/registry/default/example/copy-loading-states.tsx @@ -8,13 +8,13 @@ export default function CopyLoadingStates() {
Bad Example
- - -
@@ -22,13 +22,13 @@ export default function CopyLoadingStates() {
Good Example
- - -
diff --git a/apps/design-system/registry/default/example/copy-tooltips.tsx b/apps/design-system/registry/default/example/copy-tooltips.tsx index 2e564e5c813..9faeb3c3243 100644 --- a/apps/design-system/registry/default/example/copy-tooltips.tsx +++ b/apps/design-system/registry/default/example/copy-tooltips.tsx @@ -13,7 +13,7 @@ export default function CopyTooltips() { @@ -303,7 +303,7 @@ export default function DataTableDemo() {
- - + + + - - + + ) } diff --git a/apps/design-system/registry/default/example/date-picker-form.tsx b/apps/design-system/registry/default/example/date-picker-form.tsx index 3e04211b3e4..52aa14fc345 100644 --- a/apps/design-system/registry/default/example/date-picker-form.tsx +++ b/apps/design-system/registry/default/example/date-picker-form.tsx @@ -2,27 +2,26 @@ import { zodResolver } from '@hookform/resolvers/zod' import { format } from 'date-fns' -import { CalendarIcon } from 'lucide-react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Calendar, Form, - FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, - Popover, - PopoverContent, - PopoverTrigger, } from 'ui' +import { + DatePicker, + DatePickerButton, + DatePickerContent, + DatePickerTrigger, +} from 'ui-patterns/DatePicker' import { z } from 'zod' -import { cn } from '@/lib/utils' - const FormSchema = z.object({ dob: z.date({ required_error: 'A date of birth is required.', @@ -50,25 +49,16 @@ export default function DatePickerForm() { ( + render={({ field, fieldState }) => ( Date of birth - - - - - - - + + + + {field.value ? format(field.value, 'PPP') : 'Pick a date'} + + + date > new Date() || date < new Date('1900-01-01')} initialFocus /> - - + + Your date of birth is used to calculate your age. )} /> - + ) diff --git a/apps/design-system/registry/default/example/date-picker-with-presets.tsx b/apps/design-system/registry/default/example/date-picker-with-presets.tsx index 0641cad33b1..cb252655d34 100644 --- a/apps/design-system/registry/default/example/date-picker-with-presets.tsx +++ b/apps/design-system/registry/default/example/date-picker-with-presets.tsx @@ -1,41 +1,26 @@ 'use client' import { addDays, format } from 'date-fns' -import { Calendar as CalendarIcon } from 'lucide-react' import * as React from 'react' +import { Calendar, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'ui' import { - Button, - Calendar, - Popover, - PopoverContent, - PopoverTrigger, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from 'ui' - -import { cn } from '@/lib/utils' + DatePicker, + DatePickerButton, + DatePickerContent, + DatePickerTrigger, +} from 'ui-patterns/DatePicker' export default function DatePickerWithPresets() { const [date, setDate] = React.useState() return ( - - - - - + + +
- @@ -43,7 +43,7 @@ export default function DialogCloseButton() { - diff --git a/apps/design-system/registry/default/example/dialog-demo.tsx b/apps/design-system/registry/default/example/dialog-demo.tsx index 455dd39f56e..9d6972d7b63 100644 --- a/apps/design-system/registry/default/example/dialog-demo.tsx +++ b/apps/design-system/registry/default/example/dialog-demo.tsx @@ -17,7 +17,7 @@ export default function DialogDemo() { return ( - + diff --git a/apps/design-system/registry/default/example/drawer-demo.tsx b/apps/design-system/registry/default/example/drawer-demo.tsx index c1fd6d08a64..22ce3dd8566 100644 --- a/apps/design-system/registry/default/example/drawer-demo.tsx +++ b/apps/design-system/registry/default/example/drawer-demo.tsx @@ -67,7 +67,7 @@ export default function DrawerDemo() { return ( - @@ -80,7 +80,7 @@ export default function DrawerDemo() {
- +
diff --git a/apps/design-system/registry/default/example/drawer-dialog.tsx b/apps/design-system/registry/default/example/drawer-dialog.tsx index c80bd29914f..b34a0df1ca9 100644 --- a/apps/design-system/registry/default/example/drawer-dialog.tsx +++ b/apps/design-system/registry/default/example/drawer-dialog.tsx @@ -32,7 +32,7 @@ export default function DrawerDialogDemo() { return ( - + @@ -50,7 +50,7 @@ export default function DrawerDialogDemo() { return ( - + @@ -62,7 +62,7 @@ export default function DrawerDialogDemo() { - + @@ -81,7 +81,7 @@ function ProfileForm({ className }: React.ComponentProps<'form'>) {
- + ) } diff --git a/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx b/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx index 900ebe6903b..b830175419d 100644 --- a/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx +++ b/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx @@ -22,7 +22,7 @@ export default function DropdownMenuCheckboxes() { return ( - + Appearance diff --git a/apps/design-system/registry/default/example/dropdown-menu-demo.tsx b/apps/design-system/registry/default/example/dropdown-menu-demo.tsx index 56829f81456..40ca46ba1d3 100644 --- a/apps/design-system/registry/default/example/dropdown-menu-demo.tsx +++ b/apps/design-system/registry/default/example/dropdown-menu-demo.tsx @@ -34,7 +34,7 @@ export default function DropdownMenuDemo() { return ( - + My Account diff --git a/apps/design-system/registry/default/example/dropdown-menu-radio-group.tsx b/apps/design-system/registry/default/example/dropdown-menu-radio-group.tsx index c9b7ab86b89..311bda5f61a 100644 --- a/apps/design-system/registry/default/example/dropdown-menu-radio-group.tsx +++ b/apps/design-system/registry/default/example/dropdown-menu-radio-group.tsx @@ -18,7 +18,7 @@ export default function DropdownMenuRadioGroupDemo() { return ( - + Panel Position diff --git a/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx b/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx index 5c45d693e64..81832e46c20 100644 --- a/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx +++ b/apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx @@ -4,7 +4,7 @@ import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, Tabl export default function EmptyStateZeroItemsTable() { return (
- diff --git a/apps/design-system/registry/default/example/empty-state-missing-route.tsx b/apps/design-system/registry/default/example/empty-state-missing-route.tsx index 004dd0e1cbc..5f5c716b9a6 100644 --- a/apps/design-system/registry/default/example/empty-state-missing-route.tsx +++ b/apps/design-system/registry/default/example/empty-state-missing-route.tsx @@ -13,7 +13,7 @@ export default function EmptyStateMissingRoute() { title="Unable to find bucket" description={`${bucketId ? `The bucket “${bucketId}”` : 'This bucket'} doesn’t seem to exist.`} > - @@ -29,7 +29,7 @@ export default function EmptyStatePresentationalIcon() { title="Add a provider" description="Use third-party authentication systems to access your project." > - diff --git a/apps/design-system/registry/default/example/empty-state-presentational-demo.tsx b/apps/design-system/registry/default/example/empty-state-presentational-demo.tsx index 7b79318535a..cf33f0e62f3 100644 --- a/apps/design-system/registry/default/example/empty-state-presentational-demo.tsx +++ b/apps/design-system/registry/default/example/empty-state-presentational-demo.tsx @@ -8,7 +8,7 @@ export default function EmptyStatePresentationalIcon() { title="Create an auth hook" description="Use Postgres functions or HTTP endpoints to customize your authentication flow." > - diff --git a/apps/design-system/registry/default/example/empty-state-presentational-icon.tsx b/apps/design-system/registry/default/example/empty-state-presentational-icon.tsx index 2da35261df0..ba9e158d59b 100644 --- a/apps/design-system/registry/default/example/empty-state-presentational-icon.tsx +++ b/apps/design-system/registry/default/example/empty-state-presentational-icon.tsx @@ -10,7 +10,7 @@ export default function EmptyStatePresentationalIcon() { title="Create a vector bucket" description="Store, index, and query your vector embeddings at scale." > - diff --git a/apps/design-system/registry/default/example/field-demo.tsx b/apps/design-system/registry/default/example/field-demo.tsx index 09fe7a26a82..7f73f348158 100644 --- a/apps/design-system/registry/default/example/field-demo.tsx +++ b/apps/design-system/registry/default/example/field-demo.tsx @@ -112,8 +112,8 @@ export default function FieldDemo() { - - + diff --git a/apps/design-system/registry/default/example/field-responsive.tsx b/apps/design-system/registry/default/example/field-responsive.tsx index 1f00b1a2835..568e583485d 100644 --- a/apps/design-system/registry/default/example/field-responsive.tsx +++ b/apps/design-system/registry/default/example/field-responsive.tsx @@ -43,8 +43,8 @@ export default function FieldResponsive() { - - + diff --git a/apps/design-system/registry/default/example/filter-bar-demo.tsx b/apps/design-system/registry/default/example/filter-bar-demo.tsx index 0b11a8889df..497a1ddecd8 100644 --- a/apps/design-system/registry/default/example/filter-bar-demo.tsx +++ b/apps/design-system/registry/default/example/filter-bar-demo.tsx @@ -25,11 +25,11 @@ function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) { className="w-full" />
- diff --git a/apps/design-system/registry/default/example/form-item-layout-before-label.tsx b/apps/design-system/registry/default/example/form-item-layout-before-label.tsx index a3c7813ec3d..22e5dd8ebbf 100644 --- a/apps/design-system/registry/default/example/form-item-layout-before-label.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-before-label.tsx @@ -42,7 +42,7 @@ export default function FormItemLayoutDemo() { )} /> - diff --git a/apps/design-system/registry/default/example/form-item-layout-demo.tsx b/apps/design-system/registry/default/example/form-item-layout-demo.tsx index 6adef61bc1c..3c448902ab5 100644 --- a/apps/design-system/registry/default/example/form-item-layout-demo.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-demo.tsx @@ -45,7 +45,7 @@ export default function FormItemLayoutDemo() { )} /> - diff --git a/apps/design-system/registry/default/example/form-item-layout-with-checkbox-list.tsx b/apps/design-system/registry/default/example/form-item-layout-with-checkbox-list.tsx index f9918829395..75eaccf274e 100644 --- a/apps/design-system/registry/default/example/form-item-layout-with-checkbox-list.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-with-checkbox-list.tsx @@ -95,7 +95,7 @@ export default function FormItemLayoutDemo() { )} /> - diff --git a/apps/design-system/registry/default/example/form-item-layout-with-checkbox.tsx b/apps/design-system/registry/default/example/form-item-layout-with-checkbox.tsx index eed49414ff1..7768d0a9559 100644 --- a/apps/design-system/registry/default/example/form-item-layout-with-checkbox.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-with-checkbox.tsx @@ -39,7 +39,7 @@ export default function FormItemLayoutDemo() { )} /> - diff --git a/apps/design-system/registry/default/example/form-item-layout-with-horizontal.tsx b/apps/design-system/registry/default/example/form-item-layout-with-horizontal.tsx index 73cf02f14ba..4ca0348f374 100644 --- a/apps/design-system/registry/default/example/form-item-layout-with-horizontal.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-with-horizontal.tsx @@ -48,7 +48,7 @@ export default function FormItemLayoutDemo() { />
-
diff --git a/apps/design-system/registry/default/example/form-item-layout-with-select.tsx b/apps/design-system/registry/default/example/form-item-layout-with-select.tsx index 171cff0daf9..735bfdfccaf 100644 --- a/apps/design-system/registry/default/example/form-item-layout-with-select.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-with-select.tsx @@ -62,7 +62,7 @@ export default function FormItemLayoutDemo() { )} /> - diff --git a/apps/design-system/registry/default/example/form-item-layout-with-switch.tsx b/apps/design-system/registry/default/example/form-item-layout-with-switch.tsx index f0bd8a5f991..c99009750a4 100644 --- a/apps/design-system/registry/default/example/form-item-layout-with-switch.tsx +++ b/apps/design-system/registry/default/example/form-item-layout-with-switch.tsx @@ -40,7 +40,7 @@ export default function FormItemLayoutDemo() { )} /> - diff --git a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx index 5fa534c4054..1d1820c157c 100644 --- a/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx +++ b/apps/design-system/registry/default/example/form-patterns-pagelayout.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { format } from 'date-fns' -import { CalendarIcon, ExternalLink, Trash, Upload } from 'lucide-react' +import { ExternalLink, Trash, Upload } from 'lucide-react' import { useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { @@ -19,9 +19,6 @@ import { InputGroup, InputGroupAddon, InputGroupText, - Popover, - PopoverContent, - PopoverTrigger, RadioGroupStacked, RadioGroupStackedItem, Select, @@ -33,6 +30,12 @@ import { Textarea, } from 'ui' import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input' +import { + DatePicker, + DatePickerButton, + DatePickerContent, + DatePickerTrigger, +} from 'ui-patterns/DatePicker' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray' import { getKeyValueFieldArrayValidationIssues } from 'ui-patterns/form/KeyValueFieldArray/validation' @@ -57,7 +60,15 @@ const formSchema = z .object({ name: z.string().min(1, 'Name is required'), description: z.string().optional(), - maxConnections: z.number().min(1).max(1000), + maxConnections: z + .union([ + z.literal(''), + z.coerce + .number() + .gte(1000, 'Max connections should be at least 1000') + .lte(10000, 'Max connections should not exceed 10000'), + ]) + .refine((value) => value !== '', 'Max connections is required'), enableFeature: z.boolean(), enableRls: z.boolean(), enableNotifications: z.boolean(), @@ -65,9 +76,17 @@ const formSchema = z region: z.string().min(1, 'Region is required'), schemas: z.array(z.string()).min(1, 'At least one schema is required'), queueType: z.enum(['basic', 'partitioned']), - expiryDate: z.date().optional(), + expiryDate: z.date(), password: z.string().min(8, 'Password must be at least 8 characters'), - duration: z.number().min(5).max(30), + duration: z + .union([ + z.literal(''), + z.coerce + .number() + .gte(1000, 'Duration should be at least 5ms') + .lte(10000, 'Duration should not exceed 30ms'), + ]) + .refine((value) => value !== '', 'Duration is required'), redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })), httpHeaders: z.array(z.object({ key: z.string().trim(), value: z.string().trim() })), apiKey: z.string().optional(), @@ -211,13 +230,7 @@ export default function FormPatternsPageLayout() { description="Numeric input with min/max validation" > - field.onChange(Number(e.target.value))} - /> + )} @@ -237,15 +250,9 @@ export default function FormPatternsPageLayout() { > - field.onChange(Number(e.target.value))} - type="number" - min={5} - max={30} - /> + - MB + ms @@ -320,19 +327,20 @@ export default function FormPatternsPageLayout() { >
- +
{logoUrl && ( - - + + + - - + + )} @@ -753,13 +757,13 @@ export default function FormPatternsPageLayout() { >
-
@@ -767,11 +771,11 @@ export default function FormPatternsPageLayout() { {form.formState.isDirty && ( - )} - diff --git a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx index 41e6c2c18a3..1c979983da9 100644 --- a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx +++ b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { format } from 'date-fns' -import { CalendarIcon, ExternalLink, Trash, Upload } from 'lucide-react' +import { ExternalLink, Trash, Upload } from 'lucide-react' import { useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { @@ -14,11 +14,7 @@ import { Input, InputGroup, InputGroupAddon, - InputGroupInput, InputGroupText, - Popover, - PopoverContent, - PopoverTrigger, RadioGroupStacked, RadioGroupStackedItem, Select, @@ -37,6 +33,12 @@ import { Textarea, } from 'ui' import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input' +import { + DatePicker, + DatePickerButton, + DatePickerContent, + DatePickerTrigger, +} from 'ui-patterns/DatePicker' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray' import { getKeyValueFieldArrayValidationIssues } from 'ui-patterns/form/KeyValueFieldArray/validation' @@ -54,7 +56,15 @@ const formSchema = z .object({ name: z.string().min(1, 'Name is required'), description: z.string().optional(), - maxConnections: z.number().min(1).max(1000), + maxConnections: z + .union([ + z.literal(''), + z.coerce + .number() + .gte(1000, 'Max connections should be at least 1000') + .lte(10000, 'Max connections should not exceed 10000'), + ]) + .refine((value) => value !== '', 'Max connections is required'), enableFeature: z.boolean(), enableRls: z.boolean(), enableNotifications: z.boolean(), @@ -62,9 +72,17 @@ const formSchema = z region: z.string().min(1, 'Region is required'), schemas: z.array(z.string()).min(1, 'At least one schema is required'), queueType: z.enum(['basic', 'partitioned']), - expiryDate: z.date().optional(), + expiryDate: z.date(), password: z.string().min(8, 'Password must be at least 8 characters'), - duration: z.number().min(5).max(30), + duration: z + .union([ + z.literal(''), + z.coerce + .number() + .gte(1000, 'Duration should be at least 5ms') + .lte(10000, 'Duration should not exceed 30ms'), + ]) + .refine((value) => value !== '', 'Duration is required'), redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })), httpHeaders: z.array(z.object({ key: z.string().trim(), value: z.string().trim() })), apiKey: z.string().optional(), @@ -127,7 +145,7 @@ export default function FormPatternsSidePanel() { return ( <> - @@ -160,7 +178,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Password Input */} @@ -181,7 +199,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Copyable Input */} @@ -209,7 +227,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Number Input */} @@ -223,20 +241,14 @@ export default function FormPatternsSidePanel() { description="Numeric input with min/max validation" > - field.onChange(Number(e.target.value))} - /> + )} /> - + {/* Input with Units */} @@ -253,7 +265,7 @@ export default function FormPatternsSidePanel() { - MB + ms @@ -262,7 +274,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Textarea */} @@ -288,7 +300,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Icon Upload */} @@ -306,7 +318,7 @@ export default function FormPatternsSidePanel() {
{logoUrl && ( - - + + + - - + + )} /> - + {/* Field Array */} @@ -717,7 +726,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Key/Value Field Array */} @@ -748,7 +757,7 @@ export default function FormPatternsSidePanel() { /> - + {/* Action Field */} @@ -759,13 +768,13 @@ export default function FormPatternsSidePanel() { >
-
@@ -775,7 +784,7 @@ export default function FormPatternsSidePanel() { - diff --git a/apps/design-system/registry/default/example/hover-card-demo.tsx b/apps/design-system/registry/default/example/hover-card-demo.tsx index 251268b2960..2ed023b6710 100644 --- a/apps/design-system/registry/default/example/hover-card-demo.tsx +++ b/apps/design-system/registry/default/example/hover-card-demo.tsx @@ -13,7 +13,7 @@ export default function HoverCardDemo() { return ( - +
diff --git a/apps/design-system/registry/default/example/inner-side-menu-empty.tsx b/apps/design-system/registry/default/example/inner-side-menu-empty.tsx index bf4bf8b2fe8..0dfb4c6d029 100644 --- a/apps/design-system/registry/default/example/inner-side-menu-empty.tsx +++ b/apps/design-system/registry/default/example/inner-side-menu-empty.tsx @@ -34,7 +34,7 @@ export default function InnerSideMenuEmpty() { description="Create your first serverless function to get started." illustration={
🚀
} actions={ - } @@ -62,7 +62,7 @@ export default function InnerSideMenuEmpty() { } actions={ - } diff --git a/apps/design-system/registry/default/example/input-form.tsx b/apps/design-system/registry/default/example/input-form.tsx index d0d229600d6..2bb3773c45f 100644 --- a/apps/design-system/registry/default/example/input-form.tsx +++ b/apps/design-system/registry/default/example/input-form.tsx @@ -57,7 +57,7 @@ export default function InputForm() { )} /> - diff --git a/apps/design-system/registry/default/example/input-otp-form.tsx b/apps/design-system/registry/default/example/input-otp-form.tsx index 8a76ba413b6..41c6cb342b8 100644 --- a/apps/design-system/registry/default/example/input-otp-form.tsx +++ b/apps/design-system/registry/default/example/input-otp-form.tsx @@ -71,7 +71,7 @@ export default function InputOTPForm() { )} /> - + ) diff --git a/apps/design-system/registry/default/example/input-with-button.tsx b/apps/design-system/registry/default/example/input-with-button.tsx index 739d2f0ea19..d948c6f37ff 100644 --- a/apps/design-system/registry/default/example/input-with-button.tsx +++ b/apps/design-system/registry/default/example/input-with-button.tsx @@ -4,7 +4,7 @@ export default function InputWithButton() { return (
-
diff --git a/apps/design-system/registry/default/example/key-value-field-array-demo.tsx b/apps/design-system/registry/default/example/key-value-field-array-demo.tsx index ba214ed192d..fa6a99f7015 100644 --- a/apps/design-system/registry/default/example/key-value-field-array-demo.tsx +++ b/apps/design-system/registry/default/example/key-value-field-array-demo.tsx @@ -64,7 +64,7 @@ export default function KeyValueFieldArrayDemo() {
-
diff --git a/apps/design-system/registry/default/example/keyboard-shortcut-demo.tsx b/apps/design-system/registry/default/example/keyboard-shortcut-demo.tsx index 5d1dc8919ef..022667bee56 100644 --- a/apps/design-system/registry/default/example/keyboard-shortcut-demo.tsx +++ b/apps/design-system/registry/default/example/keyboard-shortcut-demo.tsx @@ -12,7 +12,7 @@ export default function KeyboardShortcutDemo() {
Limit: {limit} -
diff --git a/apps/design-system/registry/default/example/navigation-menu-demo.tsx b/apps/design-system/registry/default/example/navigation-menu-demo.tsx index c36b5c6bd88..66a6fa24fac 100644 --- a/apps/design-system/registry/default/example/navigation-menu-demo.tsx +++ b/apps/design-system/registry/default/example/navigation-menu-demo.tsx @@ -57,7 +57,9 @@ export default function NavigationMenuDemo() { - + Getting started @@ -90,7 +92,9 @@ export default function NavigationMenuDemo() { - + Components @@ -106,7 +110,7 @@ export default function NavigationMenuDemo() { Documentation diff --git a/apps/design-system/registry/default/example/navigation-menu-responsive.tsx b/apps/design-system/registry/default/example/navigation-menu-responsive.tsx index b5c4b309fd0..fdd08d09365 100644 --- a/apps/design-system/registry/default/example/navigation-menu-responsive.tsx +++ b/apps/design-system/registry/default/example/navigation-menu-responsive.tsx @@ -91,7 +91,9 @@ export default function NavigationMenuDemo() { - + Components @@ -105,7 +107,9 @@ export default function NavigationMenuDemo() { - + Components @@ -119,7 +123,9 @@ export default function NavigationMenuDemo() { - + Components @@ -133,7 +139,9 @@ export default function NavigationMenuDemo() { - + Components @@ -149,7 +157,7 @@ export default function NavigationMenuDemo() { Documentation diff --git a/apps/design-system/registry/default/example/page-breadcrumbs-demo.tsx b/apps/design-system/registry/default/example/page-breadcrumbs-demo.tsx index 52f7576f8a4..66d569de0e2 100644 --- a/apps/design-system/registry/default/example/page-breadcrumbs-demo.tsx +++ b/apps/design-system/registry/default/example/page-breadcrumbs-demo.tsx @@ -15,7 +15,7 @@ export default function PageBreadcrumbsDemo() { - diff --git a/apps/design-system/registry/default/example/page-header-demo.tsx b/apps/design-system/registry/default/example/page-header-demo.tsx index 2122eec82db..87fc94bcbb8 100644 --- a/apps/design-system/registry/default/example/page-header-demo.tsx +++ b/apps/design-system/registry/default/example/page-header-demo.tsx @@ -27,10 +27,10 @@ export default function PageHeaderDemo() { - - diff --git a/apps/design-system/registry/default/example/page-layout-auth-emails.tsx b/apps/design-system/registry/default/example/page-layout-auth-emails.tsx index 80b4d2ddd2d..34e10c0ec80 100644 --- a/apps/design-system/registry/default/example/page-layout-auth-emails.tsx +++ b/apps/design-system/registry/default/example/page-layout-auth-emails.tsx @@ -196,7 +196,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) { layout="horizontal" className="mb-4" actions={ - } @@ -272,13 +272,13 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) { })} {notificationsForm.formState.isDirty && ( - )} )} - diff --git a/apps/design-system/registry/default/example/page-layout-detail.tsx b/apps/design-system/registry/default/example/page-layout-detail.tsx index d00260180b6..da13e6047dc 100644 --- a/apps/design-system/registry/default/example/page-layout-detail.tsx +++ b/apps/design-system/registry/default/example/page-layout-detail.tsx @@ -82,7 +82,7 @@ export default function PageLayoutDetail() {

March 15, 2024

-
@@ -114,7 +114,7 @@ export default function PageLayoutDetail() {

$234.50

-
@@ -146,7 +146,7 @@ export default function PageLayoutDetail() {

12/2025

-
diff --git a/apps/design-system/registry/default/example/page-layout-edge-function.tsx b/apps/design-system/registry/default/example/page-layout-edge-function.tsx index 7920f39bb51..1a9aeeaa5bc 100644 --- a/apps/design-system/registry/default/example/page-layout-edge-function.tsx +++ b/apps/design-system/registry/default/example/page-layout-edge-function.tsx @@ -234,10 +234,10 @@ export default function PageLayoutEdgeFunction() { - - @@ -394,7 +394,7 @@ function OverviewPage() { {CHART_INTERVALS.map((item, index) => ( @@ -703,7 +703,7 @@ function CodePage() {

Files

-
@@ -735,7 +735,7 @@ function CodePage() {
diff --git a/apps/design-system/registry/default/example/page-layout-list-simple.tsx b/apps/design-system/registry/default/example/page-layout-list-simple.tsx index 766d91201d9..5c9766ed78e 100644 --- a/apps/design-system/registry/default/example/page-layout-list-simple.tsx +++ b/apps/design-system/registry/default/example/page-layout-list-simple.tsx @@ -63,7 +63,7 @@ export default function PageLayoutListSimple() { - @@ -92,7 +92,7 @@ export default function PageLayoutListSimple() { - diff --git a/apps/design-system/registry/default/example/page-layout-list.tsx b/apps/design-system/registry/default/example/page-layout-list.tsx index 12315fdc206..225c0428bcc 100644 --- a/apps/design-system/registry/default/example/page-layout-list.tsx +++ b/apps/design-system/registry/default/example/page-layout-list.tsx @@ -102,7 +102,7 @@ export default function PageLayoutList() {
- +
@@ -126,7 +126,7 @@ export default function PageLayoutList() { - diff --git a/apps/design-system/registry/default/example/page-layout-logs-content.tsx b/apps/design-system/registry/default/example/page-layout-logs-content.tsx index c97b4eebeec..6826cb565fc 100644 --- a/apps/design-system/registry/default/example/page-layout-logs-content.tsx +++ b/apps/design-system/registry/default/example/page-layout-logs-content.tsx @@ -118,10 +118,10 @@ export function PageLayoutLogsContent() { />
- -
diff --git a/apps/design-system/registry/default/example/page-layout-settings.tsx b/apps/design-system/registry/default/example/page-layout-settings.tsx index f31f82337ba..85fb5a8d518 100644 --- a/apps/design-system/registry/default/example/page-layout-settings.tsx +++ b/apps/design-system/registry/default/example/page-layout-settings.tsx @@ -161,13 +161,13 @@ export default function PageLayoutSettings() { {refreshTokenForm.formState.isDirty && ( - )} )} diff --git a/apps/design-system/registry/default/example/page-section-with-aside.tsx b/apps/design-system/registry/default/example/page-section-with-aside.tsx index 98e32f8ac9c..bd6ae3f60e7 100644 --- a/apps/design-system/registry/default/example/page-section-with-aside.tsx +++ b/apps/design-system/registry/default/example/page-section-with-aside.tsx @@ -21,10 +21,10 @@ export default function PageSectionWithAside() { - - diff --git a/apps/design-system/registry/default/example/popover-demo.tsx b/apps/design-system/registry/default/example/popover-demo.tsx index 97ef6f8b486..6495d03a289 100644 --- a/apps/design-system/registry/default/example/popover-demo.tsx +++ b/apps/design-system/registry/default/example/popover-demo.tsx @@ -4,7 +4,7 @@ export default function PopoverDemo() { return ( - +
diff --git a/apps/design-system/registry/default/example/radio-group-card-form.tsx b/apps/design-system/registry/default/example/radio-group-card-form.tsx index 1987aaca024..c873f87056f 100644 --- a/apps/design-system/registry/default/example/radio-group-card-form.tsx +++ b/apps/design-system/registry/default/example/radio-group-card-form.tsx @@ -74,7 +74,7 @@ export default function RadioGroupForm() { )} /> - diff --git a/apps/design-system/registry/default/example/radio-group-form.tsx b/apps/design-system/registry/default/example/radio-group-form.tsx index aa9368758ab..edd035db328 100644 --- a/apps/design-system/registry/default/example/radio-group-form.tsx +++ b/apps/design-system/registry/default/example/radio-group-form.tsx @@ -76,7 +76,7 @@ export default function RadioGroupForm() { )} /> - + ) diff --git a/apps/design-system/registry/default/example/radio-group-stacked-form.tsx b/apps/design-system/registry/default/example/radio-group-stacked-form.tsx index ef1a4df1cf2..bda3ef3925c 100644 --- a/apps/design-system/registry/default/example/radio-group-stacked-form.tsx +++ b/apps/design-system/registry/default/example/radio-group-stacked-form.tsx @@ -73,7 +73,7 @@ export default function RadioGroupForm() { )} /> - diff --git a/apps/design-system/registry/default/example/select-form.tsx b/apps/design-system/registry/default/example/select-form.tsx index c8089a74c3e..5fba20bbe34 100644 --- a/apps/design-system/registry/default/example/select-form.tsx +++ b/apps/design-system/registry/default/example/select-form.tsx @@ -73,7 +73,7 @@ export default function SelectForm() { )} /> - diff --git a/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx b/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx index 4270ad97ffb..d7a4aa06e3c 100644 --- a/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx +++ b/apps/design-system/registry/default/example/sheet-confirm-on-close-demo.tsx @@ -168,7 +168,7 @@ export default function SheetConfirmOnCloseDemo() { return ( <> - @@ -210,7 +210,7 @@ export default function SheetConfirmOnCloseDemo() { - + @@ -44,7 +44,7 @@ export default function SheetDemo() {
- + diff --git a/apps/design-system/registry/default/example/sheet-nonmodal.tsx b/apps/design-system/registry/default/example/sheet-nonmodal.tsx index 203b68fe4bc..fb0bbf2871e 100644 --- a/apps/design-system/registry/default/example/sheet-nonmodal.tsx +++ b/apps/design-system/registry/default/example/sheet-nonmodal.tsx @@ -14,7 +14,7 @@ export default function SheetNonmodal() { return ( - + @@ -29,7 +29,7 @@ export default function SheetNonmodal() { - + diff --git a/apps/design-system/registry/default/example/sheet-side.tsx b/apps/design-system/registry/default/example/sheet-side.tsx index aee3e308180..85011f61b49 100644 --- a/apps/design-system/registry/default/example/sheet-side.tsx +++ b/apps/design-system/registry/default/example/sheet-side.tsx @@ -24,7 +24,7 @@ export default function SheetSide() { {SHEET_SIDES.map((side) => ( - + @@ -49,7 +49,7 @@ export default function SheetSide() { - diff --git a/apps/design-system/registry/default/example/single-value-field-array-demo.tsx b/apps/design-system/registry/default/example/single-value-field-array-demo.tsx index 9c30dc9dbf7..fa7c94c5339 100644 --- a/apps/design-system/registry/default/example/single-value-field-array-demo.tsx +++ b/apps/design-system/registry/default/example/single-value-field-array-demo.tsx @@ -45,7 +45,7 @@ export default function SingleValueFieldArrayDemo() {
-
diff --git a/apps/design-system/registry/default/example/sonner-demo.tsx b/apps/design-system/registry/default/example/sonner-demo.tsx index d2687e6071e..60df9fe7902 100644 --- a/apps/design-system/registry/default/example/sonner-demo.tsx +++ b/apps/design-system/registry/default/example/sonner-demo.tsx @@ -4,7 +4,7 @@ import { Button } from 'ui' export default function SonnerDemo() { return ( - - - + ) } diff --git a/apps/design-system/registry/default/example/sonner-upload.tsx b/apps/design-system/registry/default/example/sonner-upload.tsx index 5d2fcc38b1b..6ccffee9a3b 100644 --- a/apps/design-system/registry/default/example/sonner-upload.tsx +++ b/apps/design-system/registry/default/example/sonner-upload.tsx @@ -60,7 +60,7 @@ export default function SonnerUpload() { return (
- diff --git a/apps/design-system/registry/default/example/table-actions.tsx b/apps/design-system/registry/default/example/table-actions.tsx index 3c59609353a..679cfb7cb7d 100644 --- a/apps/design-system/registry/default/example/table-actions.tsx +++ b/apps/design-system/registry/default/example/table-actions.tsx @@ -54,13 +54,13 @@ export default function TableActions() { {user.name} {user.email} - diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx index e61c211dbcd..cea3f602613 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-with-cancel-button.tsx @@ -24,7 +24,7 @@ const TextConfirmModalWithCancelButton = () => { return ( <> - { return ( <> - { return ( <> - )} /> - diff --git a/apps/design-system/registry/default/example/tooltip-demo.tsx b/apps/design-system/registry/default/example/tooltip-demo.tsx index f347278c438..d6abaf36db2 100644 --- a/apps/design-system/registry/default/example/tooltip-demo.tsx +++ b/apps/design-system/registry/default/example/tooltip-demo.tsx @@ -5,7 +5,7 @@ export default function TooltipDemo() { - +

Add to library

diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 85097c15a08..596c4fbb5a0 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -1708,6 +1708,90 @@ export const examples: Registry = [ registryDependencies: ['mermaid'], files: ['example/mermaid-basic.tsx'], }, + { + name: 'markdown-full-example', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-full-example.tsx'], + }, + { + name: 'markdown-customization', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-customization.tsx'], + }, + { + name: 'markdown-headings', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-headings.tsx'], + }, + { + name: 'markdown-paragraphs', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-paragraphs.tsx'], + }, + { + name: 'markdown-lists', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-lists.tsx'], + }, + { + name: 'markdown-links', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-links.tsx'], + }, + { + name: 'markdown-inline-code', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-inline-code.tsx'], + }, + { + name: 'markdown-blockquotes', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-blockquotes.tsx'], + }, + { + name: 'markdown-code-blocks', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-code-blocks.tsx'], + }, + { + name: 'markdown-tables', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-tables.tsx'], + }, + { + name: 'markdown-images', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-images.tsx'], + }, + { + name: 'markdown-horizontal-rules', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-horizontal-rules.tsx'], + }, + { + name: 'markdown-quote-component', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-quote-component.tsx'], + }, + { + name: 'markdown-avatar-component', + type: 'components:example', + registryDependencies: ['markdown'], + files: ['example/markdown-avatar-component.tsx'], + }, { name: 'status-code-demo', type: 'components:example', diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index ad0db20e8fc..eb1e923d280 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -29,12 +29,21 @@ yarn-error.log* public/sitemap.xml # Per-source llms files (generated by build:llms, served by www) public/llms/ -# Generated guide markdown files -public/docs/ +# Generated guide and reference markdown files +public/markdown/ public/docs.tar.gz # Copied examples folder /examples/ +# Generated reference content (built by scripts/build-reference-content.ts) +/content/reference/ + +# Downloaded TypeDoc dumps under spec/reference///. Regenerated by +# `cd apps/docs/spec && make download.tsdoc.v2`. Hand-authored files in the +# same folders (config.json, partials/) stay tracked. +/spec/reference/*/*/*.json +!/spec/reference/*/*/config.json + # Sentry Config File .env.sentry-build-plugin diff --git a/apps/docs/CONTRIBUTING.md b/apps/docs/CONTRIBUTING.md index f1fe7b92ba7..6450a90c812 100644 --- a/apps/docs/CONTRIBUTING.md +++ b/apps/docs/CONTRIBUTING.md @@ -211,6 +211,47 @@ Optionally highlight lines by using `mark=${lineNumber}`. Don't use footnotes. +### Graphs + +Render diagrams (flowcharts, sequence diagrams, entity-relationship diagrams, etc.) by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light/dark mode automatically. + +For the full list of supported diagram types and their syntax, see the [official Mermaid diagram reference](https://mermaid.js.org/intro/syntax-reference.html). + +Sequence diagram: + +````mdx +```mermaid +sequenceDiagram + participant User + participant Browser + participant Supabase + + User->>Browser: Clicks "Sign in" + Browser->>Supabase: Request authorization + Supabase->>Browser: Return token +``` +```` + +Flowchart (`flowchart` accepts a direction like `LR`, `TD`, etc.): + +````mdx +```mermaid +flowchart LR + A["content/**/*.md"] -->|Contentlayer| B[MDX] + B --> C[Rehype] + C -->|Our Plugin| D[SVG] + D -->|Base64| E[Embedded Images] +``` +```` + +A few tips: + +- Use the standard Mermaid diagram keywords (`sequenceDiagram`, `flowchart`, `erDiagram`, etc.) on the first line of the block. +- Keep diagrams focused on a single flow or concept. If a diagram gets too dense, split it into multiple smaller diagrams. +- Wrap node labels that contain special characters (`*`, `/`, spaces, punctuation) in double quotes, for example `A["content/**/*.md"]`. +- Don't hardcode colors. The component themes the diagram automatically so it matches both light and dark mode. +- Use diagrams to support the prose, not replace it. Explain the key takeaway in text near the diagram. + ### Images Images are uploaded in the `apps/docs/public/img` folder. @@ -304,7 +345,7 @@ Here are some exceptions and Supabase-specific guidelines. ### General word usage -- **Filler words**: You can often make your writing more concise by removing these words. (Some of these words can also sound patronizing.) +- **Filler words**: You can often make your writing more concise by removing these words. (Some of these words can also sound patronizing.) Most filler, marketing, and vague-verb phrases are flagged by `supa-mdx-lint` as warnings, with suggested alternatives where a direct replacement exists. Run `pnpm lint:mdx` in `apps/docs` to check your changes. - Actually - Easy, easily - Just @@ -319,8 +360,8 @@ Here are some exceptions and Supabase-specific guidelines. ### Word list -- `Backend` isn't hyphenated (not `back-end`). - `Frontend` isn't hyphenated (not `front-end`). +- `Backend` isn't hyphenated (not `back-end`). - `Login` is a noun. `Log in` is a verb. - `Postgres` is capitalized, except in code, and used instead of `PostgreSQL`. - `Setup` is a noun. `Set up` is a verb. @@ -329,6 +370,6 @@ Here are some exceptions and Supabase-specific guidelines. ## Search -Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) aggregates all content sources (eg. guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database. +Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) aggregates all content sources (for example, guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database. Search uses a hybrid of native Postgres FTS and embedding similarity search based on [`pgvector`](https://github.com/pgvector/pgvector). At runtime, a PostgREST call triggers the RPC that runs the weighted FTS search, and an [Edge Function](https://github.com/supabase/supabase/tree/master/supabase/functions) is executed to perform the embedding search. diff --git a/apps/docs/DEVELOPERS.md b/apps/docs/DEVELOPERS.md index 4413ab84577..ea96686cb08 100644 --- a/apps/docs/DEVELOPERS.md +++ b/apps/docs/DEVELOPERS.md @@ -18,7 +18,7 @@ For a complete run-down on how all of our tools work together, see the main DEVE 1. Follow the steps outlined in the Local Development section of the main [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) 2. If you work at Supabase, run `dev:secrets:pull` to pull down the internal environment variables. If you're a community member, create a `.env` file and add this line to it: `NEXT_PUBLIC_IS_PLATFORM=false` -3. Start the local docs site by navigating to `/apps/docs` and running `npm run dev` +3. Start the local docs site by navigating to `/apps/docs` and running `pnpm run dev` 4. Visit http://localhost:3001/docs in your browser - don't forget to append the `/docs` to the end 5. Your local site should look exactly like [https://supabase.com/docs](https://supabase.com/docs) @@ -31,7 +31,7 @@ To test locally, within the `apps/docs` directory: 1. Run `pnpm build:guides-markdown` 2. Run `pnpm dev` -This creates Markdown files for all routes under the `public/docs/guides` directory, ignored by Git. +This creates Markdown files for all routes under the `public/markdown/guides` directory, ignored by Git. For production this setup runs as a `prebuild` task to allow Vercel to bundle these files with middleware and functions. diff --git a/apps/docs/README.md b/apps/docs/README.md index 7803882d6c7..b961a051746 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -6,21 +6,6 @@ Supabase Reference Docs If you are a maintainer of any tools in the Supabase ecosystem, you can use this site to provide documentation for the tools & libraries that you maintain. -## Versioning - -All tools have versioned docs, which are kept in separate folders. For example, the CLI has the following folders and files: - -- `cli`: the "next" release. -- `cli_spec`: contains the DocSpec for the "next" release (see below). -- `cli_versioned_docs`: a version of the documentation for every release (including the most current version). -- `cli_versioned_sidebars`: a version of the sidebar for every release (including the most current version). - -When you release a new version of a tool, you should also release a new version of the docs. You can do this via the command line. For example, if you just released the CLI version `1.0.1`: - -``` -npm run cli:version 1.0.1 -``` - ## DocSpec We use documentation specifications which can be used to generate human-readable docs. diff --git a/apps/docs/app/api/crawlers/route.ts b/apps/docs/app/api/crawlers/route.ts index 05654511dc1..568ee6588b0 100644 --- a/apps/docs/app/api/crawlers/route.ts +++ b/apps/docs/app/api/crawlers/route.ts @@ -1,11 +1,3 @@ -import { toHtml } from 'hast-util-to-html' -import { fromMarkdown } from 'mdast-util-from-markdown' -import { mdxFromMarkdown } from 'mdast-util-mdx' -import { toHast } from 'mdast-util-to-hast' -import { mdxjs } from 'micromark-extension-mdxjs' -import { notFound } from 'next/navigation' -import { visit } from 'unist-util-visit' - import { REFERENCES } from '~/content/navigation.references' import { getFlattenedSections, @@ -16,6 +8,13 @@ import { getRefMarkdown } from '~/features/docs/Reference.mdx' import type { MethodTypes, VariableTypes } from '~/features/docs/Reference.typeSpec' import type { AbbrevApiReferenceSection } from '~/features/docs/Reference.utils' import { BASE_PATH } from '~/lib/constants' +import { toHtml } from 'hast-util-to-html' +import { fromMarkdown } from 'mdast-util-from-markdown' +import { mdxFromMarkdown } from 'mdast-util-mdx' +import { toHast } from 'mdast-util-to-hast' +import { mdxjs } from 'micromark-extension-mdxjs' +import { notFound } from 'next/navigation' +import { visit } from 'unist-util-visit' export async function GET(request: Request) { const url = new URL(request.url) @@ -138,7 +137,7 @@ async function functionDetails( let types: MethodTypes | VariableTypes | undefined if (libraryMeta.typeSpec && '$ref' in fn) { - types = await getTypeSpec(fn['$ref'] as string) + types = await getTypeSpec(lib, version ?? libraryMeta.versions[0], fn['$ref'] as string) } const fullDescription = [ @@ -151,7 +150,7 @@ async function functionDetails( .join('') const parameters = parametersToHtml(fn, types) - const examples = examplesToHtml(fn) + const examples = examplesToHtml(fn, types) return fullDescription + parameters + examples } @@ -219,13 +218,24 @@ function parametersToHtml(fn: any, types: MethodTypes | VariableTypes | undefine return result } -function examplesToHtml(fn: any) { - if (!fn.examples || fn.examples.length === 0) return '' +function examplesToHtml(fn: any, types?: MethodTypes | VariableTypes) { + // Prefer hand-authored YAML/JSON examples on the section entry; fall back to + // TSDoc-extracted `@example` blocks on the method's normalised comment. The + // page renderer in `Reference.sections.tsx` does the same merge, so the + // crawler stays consistent with what a browser sees. + const examples = + Array.isArray(fn.examples) && fn.examples.length > 0 + ? fn.examples + : (types?.comment?.examples ?? []) + if (examples.length === 0) return '' let result = '

Examples

' - result += fn.examples - .map((example) => `

${example.name ?? ''}

` + mdxToHtml(example.code ?? '')) + result += examples + .map( + (example: { name?: string; code?: string }) => + `

${example.name ?? ''}

` + mdxToHtml(example.code ?? '') + ) .join('') return result diff --git a/apps/docs/app/api/guides-md/[...slug]/route.ts b/apps/docs/app/api/guides-md/[...slug]/route.ts index 3e877b3181f..51dea28525c 100644 --- a/apps/docs/app/api/guides-md/[...slug]/route.ts +++ b/apps/docs/app/api/guides-md/[...slug]/route.ts @@ -5,7 +5,7 @@ import { NextResponse } from 'next/server' export async function GET(request: Request, { params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params - const baseDir = path.join(process.cwd(), 'public/docs/guides') + const baseDir = path.join(process.cwd(), 'public/markdown/guides') const filePath = path.join(baseDir, `${slug.join('/')}.md`) if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) { diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index 4f03645cc87..a4c0476d5d4 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -354,7 +354,7 @@ If your image has alternate light and dark versions, or you want to make it zoom ### Project Variables -Some guides and tutorials will require that users copy their Supabase project URL and anon key. You can provide those inline if the user is signed in: +Some guides and tutorials will require that users copy their Supabase project URL and publishable or secret key. You can provide those inline if the user is signed in: ```mdx diff --git a/apps/docs/app/error.tsx b/apps/docs/app/error.tsx index 088bd8ee81d..716873821f0 100644 --- a/apps/docs/app/error.tsx +++ b/apps/docs/app/error.tsx @@ -16,10 +16,10 @@ const ErrorPage = ({ error }) => { Sorry, something went wrong
- -
diff --git a/apps/docs/app/guides/database/database-advisors/page.tsx b/apps/docs/app/guides/database/database-advisors/page.tsx index e35e9da410c..56d9c89876b 100644 --- a/apps/docs/app/guides/database/database-advisors/page.tsx +++ b/apps/docs/app/guides/database/database-advisors/page.tsx @@ -25,7 +25,7 @@ const meta = { } const generateMetadata = genGuideMeta(() => ({ - pathname: '/guides/database/database-linter', + pathname: '/guides/database/database-advisors', meta, })) diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index aa0e0fd0c15..ebafbd690b7 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -32,48 +32,36 @@ const repo = 'wrappers' const docsDir = 'docs/catalog' const externalSite = 'https://supabase.github.io/wrappers' -type TagQueryResponse = { +type DocsTagsQueryResponse = { repository: { refs: { - nodes: - | { - name: string - }[] - | null - pageInfo: { - hasNextPage: boolean - endCursor: string | null - } + nodes: { name: string }[] | null + pageInfo: { hasNextPage: boolean; endCursor: string | null } } } } -const tagQuery = ` - query TagQuery($owner: String!, $name: String!, $after: String) { - repository(owner: $owner, name: $name) { - refs( - refPrefix: "refs/tags/", - orderBy: { - field: TAG_COMMIT_DATE, - direction: DESC - }, - first: 5, - after: $after - ) { - nodes { - name - } - pageInfo { - hasNextPage - endCursor - } - } +const docsTagsQuery = ` + query DocsTagsQuery($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + refs( + refPrefix: "refs/tags/", + orderBy: { field: TAG_COMMIT_DATE, direction: DESC }, + first: 5, + after: $after + ) { + nodes { name } + pageInfo { hasNextPage endCursor } } } - ` + } +` -async function getLatestRelease(after: string | null = null) { +async function getLatestDocsTag(after: string | null = null): Promise { try { + /** + * We use GraphQL as it's the only way to use `orderBy` on Github API. + */ const { repository: { refs: { @@ -81,18 +69,18 @@ async function getLatestRelease(after: string | null = null) { pageInfo: { hasNextPage, endCursor }, }, }, - } = await octokit().graphql(tagQuery, { + } = await octokit().graphql(docsTagsQuery, { owner: org, name: repo, after, }) return ( - nodes?.find((node) => node?.name?.match(/^docs_v\d+\.\d+\.\d+/))?.name ?? - (hasNextPage && endCursor ? await getLatestRelease(endCursor) : null) + nodes?.find(({ name }) => /^docs_v\d+\.\d+\.\d+/.test(name))?.name ?? + (hasNextPage && endCursor ? await getLatestDocsTag(endCursor) : null) ) } catch (error) { - console.error(`Error fetching release tags for wrappers federated pages: ${error}`) + console.error(`Error fetching docs tags for wrappers federated pages: ${error}`) return null } } @@ -123,6 +111,22 @@ const pageMap = [ }, remoteFile: 'bigquery.md', }, + { + slug: 'cal', + meta: { + title: 'Cal.com', + dashboardIntegrationPath: 'cal_wrapper', + }, + remoteFile: 'cal.md', + }, + { + slug: 'calendly', + meta: { + title: 'Calendly', + dashboardIntegrationPath: 'calendly_wrapper', + }, + remoteFile: 'calendly.md', + }, { slug: 'clerk', meta: { @@ -139,6 +143,14 @@ const pageMap = [ }, remoteFile: 'clickhouse.md', }, + { + slug: 'cloudflare-d1', + meta: { + title: 'Cloudflare D1', + dashboardIntegrationPath: 'cfd1_wrapper', + }, + remoteFile: 'cfd1.md', + }, { slug: 'cognito', meta: { @@ -151,9 +163,18 @@ const pageMap = [ slug: 'duckdb', meta: { title: 'DuckDB', + dashboardIntegrationPath: undefined, }, remoteFile: 'duckdb.md', }, + { + slug: 'dynamodb', + meta: { + title: 'AWS DynamoDB', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'dynamodb.md', + }, { slug: 'firebase', meta: { @@ -162,6 +183,22 @@ const pageMap = [ }, remoteFile: 'firebase.md', }, + { + slug: 'gravatar', + meta: { + title: 'Gravatar', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'gravatar.md', + }, + { + slug: 'hubspot', + meta: { + title: 'HubSpot', + dashboardIntegrationPath: 'hubspot_wrapper', + }, + remoteFile: 'hubspot.md', + }, { slug: 'iceberg', meta: { @@ -170,6 +207,14 @@ const pageMap = [ }, remoteFile: 'iceberg.md', }, + { + slug: 'infura', + meta: { + title: 'Infura', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'infura.md', + }, { slug: 'logflare', meta: { @@ -186,6 +231,14 @@ const pageMap = [ }, remoteFile: 'mssql.md', }, + { + slug: 'mysql', + meta: { + title: 'MySQL', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'mysql.md', + }, { slug: 'notion', meta: { @@ -194,6 +247,22 @@ const pageMap = [ }, remoteFile: 'notion.md', }, + { + slug: 'openapi', + meta: { + title: 'OpenAPI', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'openapi.md', + }, + { + slug: 'orb', + meta: { + title: 'Orb', + dashboardIntegrationPath: 'orb_wrapper', + }, + remoteFile: 'orb.md', + }, { slug: 'paddle', meta: { @@ -226,6 +295,22 @@ const pageMap = [ }, remoteFile: 's3vectors.md', }, + { + slug: 'shopify', + meta: { + title: 'Shopify', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'shopify.md', + }, + { + slug: 'slack', + meta: { + title: 'Slack', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'slack.md', + }, { slug: 'snowflake', meta: { @@ -345,9 +430,10 @@ const getContent = async (params: Params) => { let remoteFile: string ;({ remoteFile, meta } = federatedPage) - const tag = await getLatestRelease() + const tag = await getLatestDocsTag() + if (!tag) { - throw new Error('No latest release found for federated wrappers pages') + throw new Error('No latest docs tag found for federated wrappers pages') } editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}` @@ -425,7 +511,7 @@ const urlTransform: UrlTransformFunction = (url) => { } const generateStaticParams = async () => { - if (!IS_DEV) { + if (IS_DEV) { return [] } diff --git a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx index 76152dc0aa1..4053787f65e 100644 --- a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx +++ b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx @@ -87,7 +87,7 @@ const getContent = async ({ slug }: Params) => { ) return { - pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`, + pathname: `/guides/deployment/ci/${slug}` satisfies `/${string}`, meta, content, editLink, diff --git a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx index 07c76c8da40..153c814eff3 100644 --- a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx @@ -136,7 +136,7 @@ const getContent = async ({ slug }: Params) => { return { pathname: - `/guides/platform/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`, + `/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`, meta, content, editLink, diff --git a/apps/docs/app/guides/deployment/terraform/reference/page.tsx b/apps/docs/app/guides/deployment/terraform/reference/page.tsx index d703ea17cb3..8b974373fe4 100644 --- a/apps/docs/app/guides/deployment/terraform/reference/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/reference/page.tsx @@ -22,7 +22,7 @@ const meta = { } const generateMetadata = genGuideMeta(() => ({ - pathname: '/guides/platform/terraform/reference', + pathname: '/guides/deployment/terraform/reference', meta, })) diff --git a/apps/docs/app/guides/local-development/cli/config/page.tsx b/apps/docs/app/guides/local-development/cli/config/page.tsx index 124ade3a74c..8461b6baeb4 100644 --- a/apps/docs/app/guides/local-development/cli/config/page.tsx +++ b/apps/docs/app/guides/local-development/cli/config/page.tsx @@ -12,7 +12,7 @@ const meta = { } const generateMetadata = genGuideMeta(() => ({ - pathname: '/guides/cli/config', + pathname: '/guides/local-development/cli/config', meta, })) diff --git a/apps/docs/app/guides/troubleshooting/page.tsx b/apps/docs/app/guides/troubleshooting/page.tsx index d00356c6e27..19e92851bb5 100644 --- a/apps/docs/app/guides/troubleshooting/page.tsx +++ b/apps/docs/app/guides/troubleshooting/page.tsx @@ -1,5 +1,3 @@ -import { type Metadata } from 'next' - import { TroubleshootingPreview } from '~/features/docs/Troubleshooting.ui' import { TroubleshootingFilter, @@ -16,6 +14,7 @@ import { TROUBLESHOOTING_CONTAINER_ID } from '~/features/docs/Troubleshooting.ut import { SidebarSkeleton } from '~/layouts/MainSkeleton' import { PROD_URL } from '~/lib/constants' import { getCustomContent } from '~/lib/custom-content/getCustomContent' +import { type Metadata } from 'next' const { metadataTitle } = getCustomContent(['metadata:title']) diff --git a/apps/docs/app/not-found.tsx b/apps/docs/app/not-found.tsx index 14606cd8c1e..0df7cfa32b2 100644 --- a/apps/docs/app/not-found.tsx +++ b/apps/docs/app/not-found.tsx @@ -1,11 +1,9 @@ -import { type Metadata } from 'next' -import Link from 'next/link' - -import { Button } from 'ui' - import { Recommendations, SearchButton } from '~/features/recommendations/NotFound.client' import { LayoutMainContent } from '~/layouts/DefaultLayout' import { SidebarSkeleton } from '~/layouts/MainSkeleton' +import { type Metadata } from 'next' +import Link from 'next/link' +import { Button } from 'ui' export default function NotFound() { return ( @@ -19,12 +17,12 @@ export default function NotFound() {

- - + diff --git a/apps/docs/components/ButtonCard.tsx b/apps/docs/components/ButtonCard.tsx index f7df15298f3..a8e679f9b2c 100644 --- a/apps/docs/components/ButtonCard.tsx +++ b/apps/docs/components/ButtonCard.tsx @@ -1,6 +1,6 @@ -import React, { FC } from 'react' import Image from 'next/legacy/image' import Link from 'next/link' +import React, { FC } from 'react' import { cn } from 'ui' interface Props { @@ -11,6 +11,7 @@ interface Props { children?: any layout?: 'vertical' | 'horizontal' className?: string + onClick?: () => void } const ButtonCard: FC = ({ @@ -21,10 +22,12 @@ const ButtonCard: FC = ({ to, layout = 'vertical', className, + onClick, }) => { return ( IS_PLATFORM ? createClient( @@ -96,14 +94,9 @@ function Feedback({ className }: { className?: string }) { async function sendFeedbackVote(response: Response) { if (!supabase) return - - const { error } = await supabase.from('feedback').insert({ - vote: response, - page: pathname, - metadata: { - query: getSanitizedTabParams(), - }, - }) + const { error } = await supabase + .from('feedback') + .insert({ vote: response, page: pathname, metadata: { query: getSanitizedTabParams() } }) if (error) console.error(error) } @@ -128,15 +121,20 @@ function Feedback({ className }: { className?: string }) { }, 100) } - async function handleSubmit({ page, comment, title }: FeedbackFields) { - sendFeedbackComment({ - message: comment, - pathname: page, - title, - // @ts-expect-error -- can't click this button without having a state.response - isHelpful: state.response === 'yes', - team: getLinearTeam(pathname), - }) + async function handleSubmit({ comment, title }: FeedbackFields) { + if (supabase) { + const userId = (await gotrueClient.getSession()).data.session?.user?.id ?? null + const { error } = await supabase.from('feedback_comments').insert({ + page: pathname, + // @ts-expect-error -- the comment modal only opens after a vote, so state.response is set + vote: state.response, + title, + comment, + user_id: userId, + metadata: { query: getSanitizedTabParams() }, + }) + if (error) console.error(error) + } setModalOpen(false) refocusButton() } @@ -152,7 +150,7 @@ function Feedback({ className }: { className?: string }) { className="relative flex gap-2 items-center" > -
diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 95b223a9355..cca459b9f79 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -2,10 +2,10 @@ // End of third-party imports import { isFeatureEnabled, useBreakpoint } from 'common' -import { ChevronRight, Play, Sparkles } from 'lucide-react' +import { ChevronRight, Sparkles } from 'lucide-react' import { useTheme } from 'next-themes' import Link from 'next/link' -import { cn, IconBackground } from 'ui' +import { cn } from 'ui' import { IconPanel } from 'ui-patterns/IconPanel' import { getCustomContent } from '../lib/custom-content/getCustomContent' @@ -127,15 +127,12 @@ const HomePageCover = (props) => { >
-
- - -

Getting Started

+
+

Getting Started

+

+ Set up and connect a database in just a few minutes. +

-

- Set up and connect a database in just a few minutes. -

diff --git a/apps/docs/components/JwtGenerator/JwtGeneratorSimple.tsx b/apps/docs/components/JwtGenerator/JwtGeneratorSimple.tsx deleted file mode 100644 index 4ba0ae99e56..00000000000 --- a/apps/docs/components/JwtGenerator/JwtGeneratorSimple.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { KJUR } from 'jsrsasign' -import { useState } from 'react' -import { Button } from 'ui' -import { CodeBlock } from 'ui-patterns/CodeBlock' - -const JWT_HEADER = { alg: 'HS256', typ: 'JWT' } - -const generateRandomString = (length: number) => { - const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - let result = '' - - const MAX = Math.floor(256 / CHARS.length) * CHARS.length - 1 - const randomUInt8Array = new Uint8Array(1) - - for (let i = 0; i < length; i++) { - let randomNumber: number - do { - crypto.getRandomValues(randomUInt8Array) - randomNumber = randomUInt8Array[0] - } while (randomNumber > MAX) - - result += CHARS[randomNumber % CHARS.length] - } - - return result -} - -const generateKeys = () => { - const now = new Date() - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const fiveYears = new Date(now.getFullYear() + 5, now.getMonth(), now.getDate()) - const iat = Math.floor(today.valueOf() / 1000) - const exp = Math.floor(fiveYears.valueOf() / 1000) - - const anonToken = { role: 'anon', iss: 'supabase', iat, exp } - const serviceToken = { role: 'service_role', iss: 'supabase', iat, exp } - - const secret = generateRandomString(40) - const anonKey = KJUR.jws.JWS.sign(null, JWT_HEADER, anonToken, secret) - const serviceRoleKey = KJUR.jws.JWS.sign(null, JWT_HEADER, serviceToken, secret) - return { secret, anonKey, serviceRoleKey } -} - -export default function JwtGeneratorSimple() { - const [keys, setKeys] = useState(generateKeys) - - const regenerate = () => { - setKeys(generateKeys()) - } - - return ( -
-
- - - {keys.secret} - -
- -
- - - {keys.anonKey} - -
- -
- - - {keys.serviceRoleKey} - -
- - -
- ) -} diff --git a/apps/docs/components/JwtGenerator/index.tsx b/apps/docs/components/JwtGenerator/index.tsx deleted file mode 100644 index 27a49cc41cb..00000000000 --- a/apps/docs/components/JwtGenerator/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import dynamic from 'next/dynamic' -import { Suspense } from 'react' - -const DynamicJwtGeneratorSimple = dynamic(() => import('./JwtGeneratorSimple'), { ssr: false }) - -const JwtGeneratorSimple = () => { - return ( - Loading...
}> - - - ) -} - -export { JwtGeneratorSimple } diff --git a/apps/docs/components/MetricsStackCards.data.ts b/apps/docs/components/MetricsStackCards.data.ts new file mode 100644 index 00000000000..1cbd0a4a156 --- /dev/null +++ b/apps/docs/components/MetricsStackCards.data.ts @@ -0,0 +1,55 @@ +export type MetricsStackOption = { + title: string + description: string + href: string + iconKind: 'grafana' | 'datadog' | 'flame' + iconColor: string + iconBg: string + badges: { label: string; variant: 'default' | 'community' }[] +} + +export const metricsStackOptions: MetricsStackOption[] = [ + { + title: 'Grafana Cloud (SaaS)', + description: + 'Use Grafana Cloud’s managed Prometheus (works on Free + Pro tiers) and import the Supabase dashboard without running any infrastructure.', + href: '/guides/telemetry/metrics/grafana-cloud', + iconKind: 'grafana', + iconColor: '#F05A28', + iconBg: 'rgba(240,90,40,0.1)', + badges: [ + { label: 'Supabase guide', variant: 'default' }, + { label: 'Community', variant: 'community' }, + ], + }, + { + title: 'Grafana + self-hosted Prometheus', + description: + 'Run Prometheus yourself following the official installation guidance and pair it with Grafana plus our dashboard JSON and alert pack.', + href: '/guides/telemetry/metrics/grafana-self-hosted', + iconKind: 'grafana', + iconColor: '#F05A28', + iconBg: 'rgba(240,90,40,0.1)', + badges: [{ label: 'Supabase guide', variant: 'default' }], + }, + { + title: 'Datadog', + description: + 'Scrape the Metrics API with the Datadog Agent or Prometheus remote write and monitor Supabase alongside your app telemetry.', + href: 'https://docs.datadoghq.com/integrations/supabase/', + iconKind: 'datadog', + iconColor: '#632CA6', + iconBg: 'rgba(99,44,166,0.1)', + badges: [{ label: 'Community', variant: 'community' }], + }, + { + title: 'Vendor-agnostic / BYO Prometheus', + description: + 'Connect AWS AMP, Grafana Mimir, VictoriaMetrics, or any Prometheus-compatible SaaS with the same scrape job pattern.', + href: '/guides/telemetry/metrics/vendor-agnostic', + iconKind: 'flame', + iconColor: '#0BA678', + iconBg: 'rgba(11,166,120,0.1)', + badges: [{ label: 'Supabase guide', variant: 'default' }], + }, +] diff --git a/apps/docs/components/MetricsStackCards.tsx b/apps/docs/components/MetricsStackCards.tsx index 3ee2cbfb0ec..895a5b7daee 100644 --- a/apps/docs/components/MetricsStackCards.tsx +++ b/apps/docs/components/MetricsStackCards.tsx @@ -1,79 +1,15 @@ -import Link from 'next/link' import { Datadog, Grafana } from 'icons' import { Flame } from 'lucide-react' +import Link from 'next/link' import type { ReactNode } from 'react' -interface MetricsStackOption { - title: string - description: ReactNode - href: string - icon: ReactNode - iconColor: string - iconBg: string - badges: { label: string; variant: 'default' | 'community' }[] -} +import { metricsStackOptions, type MetricsStackOption } from './MetricsStackCards.data' -const metricsStackOptions: MetricsStackOption[] = [ - { - title: 'Grafana Cloud (SaaS)', - description: ( - <> - Use Grafana Cloud’s managed Prometheus (works on Free + Pro tiers) and import the Supabase - dashboard without running any infrastructure. - - ), - href: '/guides/telemetry/metrics/grafana-cloud', - icon: , - iconColor: '#F05A28', - iconBg: 'rgba(240,90,40,0.1)', - badges: [ - { label: 'Supabase guide', variant: 'default' }, - { label: 'Community', variant: 'community' }, - ], - }, - { - title: 'Grafana + self-hosted Prometheus', - description: ( - <> - Run Prometheus yourself following the official installation guidance and pair it with - Grafana plus our dashboard JSON and alert pack. - - ), - href: '/guides/telemetry/metrics/grafana-self-hosted', - icon: , - iconColor: '#F05A28', - iconBg: 'rgba(240,90,40,0.1)', - badges: [{ label: 'Supabase guide', variant: 'default' }], - }, - { - title: 'Datadog', - description: ( - <> - Scrape the Metrics API with the Datadog Agent or Prometheus remote write and monitor - Supabase alongside your app telemetry. - - ), - href: 'https://docs.datadoghq.com/integrations/supabase/', - icon: , - iconColor: '#632CA6', - iconBg: 'rgba(99,44,166,0.1)', - badges: [{ label: 'Community', variant: 'community' }], - }, - { - title: 'Vendor-agnostic / BYO Prometheus', - description: ( - <> - Connect AWS AMP, Grafana Mimir, VictoriaMetrics, or any Prometheus-compatible SaaS with the - same scrape job pattern. - - ), - href: '/guides/telemetry/metrics/vendor-agnostic', - icon: , - iconColor: '#0BA678', - iconBg: 'rgba(11,166,120,0.1)', - badges: [{ label: 'Supabase guide', variant: 'default' }], - }, -] +const ICONS: Record = { + grafana: , + datadog: , + flame: , +} export function MetricsStackCards() { return ( @@ -86,7 +22,7 @@ export function MetricsStackCards() { className="flex h-10 w-10 items-center justify-center rounded-full text-base font-semibold" style={{ color: option.iconColor, backgroundColor: option.iconBg }} > - {option.icon} + {ICONS[option.iconKind]}

{option.title}

diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx index 5ea8bb6956c..e2902b14112 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx @@ -164,7 +164,7 @@ const GlobalMobileMenu = ({ open, setOpen }: Props) => { ) : ( <> - diff --git a/apps/docs/content/_partials/user_management_quickstart_sql_template.mdx b/apps/docs/content/_partials/user_management_quickstart_sql_template.mdx index 28f790a6d37..276bdfef8ab 100644 --- a/apps/docs/content/_partials/user_management_quickstart_sql_template.mdx +++ b/apps/docs/content/_partials/user_management_quickstart_sql_template.mdx @@ -49,10 +49,10 @@ create trigger on_auth_user_created insert into storage.buckets (id, name) values ('avatars', 'avatars'); --- Set up access controls for storage. +-- Set up access controls for storage. Allows downloading object with public key -- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details. create policy "Avatar images are publicly accessible." on storage.objects - for select using (bucket_id = 'avatars'); + for select using (bucket_id = 'avatars' and storage.allow_any_operation(array['object.get_authenticated_info', 'object.get_authenticated'])); create policy "Anyone can upload an avatar." on storage.objects for insert with check (bucket_id = 'avatars'); diff --git a/apps/docs/content/errorCodes/authErrorCodes.toml b/apps/docs/content/errorCodes/authErrorCodes.toml index 8d33253b3e0..12505f7a8ac 100644 --- a/apps/docs/content/errorCodes/authErrorCodes.toml +++ b/apps/docs/content/errorCodes/authErrorCodes.toml @@ -126,7 +126,7 @@ description = "MFA challenge could not be verified -- wrong TOTP code." [mfa_verification_rejected] description = "Further MFA verification is rejected. Only returned if the MFA verification attempt hook returns a reject decision." [[mfa_verification_rejected.references]] -href = "https://supabase.com/docs/guides/auth/auth-hooks?language=add-admin-role#hook-mfa-verification-attempt" +href = "https://supabase.com/docs/guides/auth/auth-hooks/mfa-verification-hook" description = "MFA verification hook" [mfa_verified_factor_exists] diff --git a/apps/docs/content/guides/ai-tools/byo-mcp.mdx b/apps/docs/content/guides/ai-tools/byo-mcp.mdx index 88e708a8c4b..2df8f11ed23 100644 --- a/apps/docs/content/guides/ai-tools/byo-mcp.mdx +++ b/apps/docs/content/guides/ai-tools/byo-mcp.mdx @@ -75,7 +75,7 @@ const server = new McpServer({ version: '0.1.0', }) -// Register a simple addition tool +// Register an addition tool server.registerTool( 'add', { @@ -218,7 +218,9 @@ After this step, you have a fully deployed MCP server accessible from anywhere. You can find ready-to-use MCP server implementations here: -- [Simple MCP server](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/simple-mcp-server) - Basic unauthenticated example +{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} + +- [Simple MCP server](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/simple-mcp-server) - Unauthenticated example ## Resources diff --git a/apps/docs/content/guides/ai-tools/mcp.mdx b/apps/docs/content/guides/ai-tools/mcp.mdx index 9379f4fb1dd..33126fc0707 100644 --- a/apps/docs/content/guides/ai-tools/mcp.mdx +++ b/apps/docs/content/guides/ai-tools/mcp.mdx @@ -28,10 +28,18 @@ After you log in, check that the MCP server is connected. For instance, in Curso To verify the client has access to the MCP server tools, try asking it to query your project or database using natural language. For example: "What tables are there in the database? Use MCP tools." +<$Show if="docs:prompts"> + For curated, ready-to-use prompts that work well with IDEs and AI agents, see our [AI Prompts](/docs/guides/getting-started/ai-prompts) collection. + + +<$Show if="docs:agent_skills"> + Additionally, you can install Supabase agent skills alongside the MCP server, use the [Supabase Plugin for AI Coding Agents](/docs/guides/getting-started/plugins) for a combined one-step setup. + + ## Available tools The Supabase MCP server provides tools organized into feature groups. All groups except Storage are enabled by default. You can enable or disable specific groups using the [configuration panel above](#step-2-configure-your-ai-tool). @@ -52,7 +60,7 @@ The Supabase MCP server provides tools organized into feature groups. All groups ### Development - `get_project_url` - Get the API URL for a project -- `get_publishable_keys` - Get anon/public keys +- `get_publishable_keys` - Get publishable and legacy anon API keys for a project - `generate_typescript_types` - Generate TypeScript types from schema ### Edge Functions @@ -197,4 +205,4 @@ We recommend the following best practices to mitigate security risks when using ## On GitHub -The MCP server repository is available at [github.com/supabase-community/supabase-mcp](https://github.com/supabase-community/supabase-mcp). +The MCP server repository is available at [github.com/supabase/mcp](https://github.com/supabase/mcp). diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index fa6fad89c0c..5226d8849aa 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -31,15 +31,72 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {aiExamples.map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + A toolkit to perform vector similarity search on your knowledge base embeddings. + + +
+
+ + + Implement image search with the OpenAI CLIP Model and Supabase Vector. + + +
+
+ + + Generate image captions using Hugging Face. + + +
+
+ + + Generate GPT text completions using OpenAI in Edge Functions. + + +
+
+ + + Use Supabase as a Retrieval Store for your ChatGPT plugin. + + +
+
+ + + Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase. + + +
{/* */} @@ -49,13 +106,45 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {aiIntegrations.map((x) => ( -
- - {x.description} - -
- ))} +
+ + + OpenAI is an AI research and deployment company. Supabase provides a way to use OpenAI in + your applications. + + +
+
+ + + A fully managed service that offers a choice of high-performing foundation models from + leading AI companies. + + +
+
+ + + Hugging Face is an open-source provider of NLP technologies. Supabase provides a way to use + Hugging Face's models in your applications. + + +
+
+ + + LangChain is a language-agnostic, open-source, and self-hosted API for text translation, + summarization, and sentiment analysis. + + +
+
+ + + LlamaIndex is a data framework for your LLM applications. + + +
{/* */} @@ -65,32 +154,31 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {[ - { - name: 'Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector', - description: - 'Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.', - href: 'https://supabase.com/customers/berriai', - }, - { - name: 'Firecrawl switches from Pinecone to Supabase for Postgres vector embeddings', - description: - 'How Firecrawl boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector', - href: 'https://supabase.com/customers/firecrawl', - }, - { - name: 'Markprompt: GDPR-Compliant AI Chatbots for Docs and Websites', - description: - "AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions", - href: 'https://supabase.com/customers/markprompt', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + + Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS + and successfully migrated to Supabase. + + +
+
+ + + How Firecrawl boosts efficiency and accuracy of chat powered search for documentation using + Supabase with pgvector + + +
+
+ + + AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and + GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure + and privacy-focused database and authentication solutions + + +
{/* */} diff --git a/apps/docs/content/guides/ai/automatic-embeddings.mdx b/apps/docs/content/guides/ai/automatic-embeddings.mdx index e87691d6153..cb39bb5a8aa 100644 --- a/apps/docs/content/guides/ai/automatic-embeddings.mdx +++ b/apps/docs/content/guides/ai/automatic-embeddings.mdx @@ -41,7 +41,7 @@ We'll start by setting up the infrastructure needed to queue and process embeddi ### Step 1: Enable extensions -First, let's enable the required extensions: +First, enable the required extensions: { const query = new URLSearchParams({ query: inputRef.current!.value }) const projectUrl = `https://your-project-ref.supabase.co/functions/v1` - const queryURL = `${projectURL}/${query}` + const queryURL = `${projectUrl}/${query}` const eventSource = new EventSource(queryURL) eventSource.addEventListener("error", (err) => { diff --git a/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx b/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx index b6b7b05ea47..a0f89695d59 100644 --- a/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx +++ b/apps/docs/content/guides/ai/examples/huggingface-image-captioning.mdx @@ -65,7 +65,7 @@ Deno.serve(async (req) => { // Supabase API URL - env var exported by default when deployed. Deno.env.get('SUPABASE_URL') ?? '', // Supabase API SECRET KEY - env var exported by default when deployed. - Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? '' + SUPABASE_SECRET_KEYS['default'] ?? '' ) // Construct image url from storage diff --git a/apps/docs/content/guides/ai/examples/nextjs-vector-search.mdx b/apps/docs/content/guides/ai/examples/nextjs-vector-search.mdx index 6bf0c7d4c87..28a4b0f6684 100644 --- a/apps/docs/content/guides/ai/examples/nextjs-vector-search.mdx +++ b/apps/docs/content/guides/ai/examples/nextjs-vector-search.mdx @@ -24,7 +24,7 @@ We assume that you have a Next.js project with a collection of `.mdx` files nest ## Prepare the database -Let's prepare the database schema. We can use the "OpenAI Vector Search" quickstart in the [SQL Editor](/dashboard/project/_/sql), or you can copy/paste the SQL below and run it yourself. +Prepare the database schema. We can use the "OpenAI Vector Search" quickstart in the [SQL Editor](/dashboard/project/_/sql), or you can copy/paste the SQL below and run it yourself. diff --git a/apps/docs/content/guides/ai/going-to-prod.mdx b/apps/docs/content/guides/ai/going-to-prod.mdx index ff7ceb25647..cf422a2c101 100644 --- a/apps/docs/content/guides/ai/going-to-prod.mdx +++ b/apps/docs/content/guides/ai/going-to-prod.mdx @@ -93,7 +93,7 @@ First, a few generic tips which you can pick and choose from: 2. Prefer `inner-product` to `L2` or `Cosine` distances if your vectors are normalized (like `text-embedding-ada-002`). If embeddings are not normalized, `Cosine` distance should give the best results with an index. 3. **Pre-warm your database.** Implement the warm-up technique before transitioning to production or running benchmarks. - Use [pg_prewarm](https://www.postgresql.org/docs/current/pgprewarm.html) to load the index into RAM `select pg_prewarm('vecs.docs_vec_idx');`. This will help to avoid cold cache issues. - - Execute 10,000 to 50,000 "warm-up" queries before each benchmark/prod. This will help to utilize cache and buffers more efficiently. + - Execute 10,000 to 50,000 "warm-up" queries before each benchmark or prod. This helps to use cache and buffers more efficiently. 4. **Establish your workload.** Fine-tune `m` and `ef_construction` or `lists` constants for the pgvector index to accelerate your queries (at the expense of a slower build times). For instance, for benchmarks with 1,000,000 OpenAI embeddings, we set `m` and `ef_construction` to 32 and 80, and it resulted in 35% higher QPS than 24 and 56 values respectively. 5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the index build parameters, balancing accuracy with queries per second (QPS). diff --git a/apps/docs/content/guides/ai/hugging-face.mdx b/apps/docs/content/guides/ai/hugging-face.mdx index 082a73a1370..0fb7fd4cdfa 100644 --- a/apps/docs/content/guides/ai/hugging-face.mdx +++ b/apps/docs/content/guides/ai/hugging-face.mdx @@ -91,7 +91,7 @@ Create a file called `.env.local` to store your Hugging Face access token: HUGGING_FACE_ACCESS_TOKEN= ``` -Let's modify the Edge Function to import Hugging Face's inference client and perform a `text-to-image` request: +Modify the Edge Function to import Hugging Face's inference client and perform a `text-to-image` request: ```ts import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2' @@ -125,7 +125,7 @@ Deno.serve(async (req) => { 1. The `image` result returned from the API will be a `Blob`. We can pass the `Blob` directly into a `new Response()` which will automatically set the content type and body of the response from the `image`. -Finally let's serve the Edge Function locally to test it: +Finally, serve the Edge Function locally to test it: ```shell supabase functions serve --env-file .env.local --no-verify-jwt diff --git a/apps/docs/content/guides/ai/hybrid-search.mdx b/apps/docs/content/guides/ai/hybrid-search.mdx index 44a682da120..70d44c1fa9b 100644 --- a/apps/docs/content/guides/ai/hybrid-search.mdx +++ b/apps/docs/content/guides/ai/hybrid-search.mdx @@ -44,7 +44,7 @@ This constant can be any positive number, but is typically small. A constant of ## Hybrid search in Postgres -Let's implement hybrid search in Postgres using `tsvector` (keyword search) and `pgvector` (semantic search). +Implement hybrid search in Postgres using `tsvector` (keyword search) and `pgvector` (semantic search). First we'll create a `documents` table to store the documents that we will search over. This is just an example - adjust this to match the structure of your application. @@ -131,7 +131,7 @@ limit $$; ``` -Let's break this down: +Where: - **Parameters:** The function accepts quite a few parameters, but the main (required) ones are `query_text`, `query_embedding`, and `match_count`. - `query_text` is the user's query text (more on this shortly) diff --git a/apps/docs/content/guides/ai/integrations/roboflow.mdx b/apps/docs/content/guides/ai/integrations/roboflow.mdx index 2b9d61fc7ac..660fd73acfa 100644 --- a/apps/docs/content/guides/ai/integrations/roboflow.mdx +++ b/apps/docs/content/guides/ai/integrations/roboflow.mdx @@ -41,7 +41,7 @@ An inference server will be available at `http://localhost:9001`. ### Step 2: Run inference on an image -You can run inference on images and videos. Let's run inference on an image. +You can run inference on images and videos. Create a new Python file and add the following code: diff --git a/apps/docs/content/guides/ai/keyword-search.mdx b/apps/docs/content/guides/ai/keyword-search.mdx index 9ee03877831..643fa08025f 100644 --- a/apps/docs/content/guides/ai/keyword-search.mdx +++ b/apps/docs/content/guides/ai/keyword-search.mdx @@ -8,7 +8,7 @@ sidebar_label: 'Keyword search' Keyword search involves locating documents or records that contain specific words or phrases, primarily based on the exact match between the search terms and the text within the data. It differs from [semantic search](/docs/guides/ai/semantic-search), which interprets the meaning behind the query to provide results that are contextually related, even if the exact words aren't present in the text. Semantic search considers synonyms, intent, and natural language nuances to provide a more nuanced approach to information retrieval. -In Postgres, keyword search is implemented using [full-text search](/docs/guides/database/full-text-search). It supports indexing and text analysis for data retrieval, focusing on records that match the search criteria. Postgres' full-text search extends beyond simple keyword matching to address linguistic nuances, making it effective for applications that require precise text queries. +In Postgres, keyword search is implemented using [full-text search](/docs/guides/database/full-text-search). It supports indexing and text analysis for data retrieval, focusing on records that match the search criteria. Postgres' full-text search extends beyond keyword matching to address linguistic nuances, making it effective for applications that require precise text queries. ## When and why to use keyword search diff --git a/apps/docs/content/guides/ai/langchain.mdx b/apps/docs/content/guides/ai/langchain.mdx index b089cb4ab5e..09cf1c570fa 100644 --- a/apps/docs/content/guides/ai/langchain.mdx +++ b/apps/docs/content/guides/ai/langchain.mdx @@ -108,7 +108,7 @@ export const run = async () => { } ``` -### Simple metadata filtering +### Basic metadata filtering [#simple-metadata-filtering] Given the above `match_documents` Postgres function, you can also pass a filter parameter to only return documents with a specific metadata field value. This filter parameter is a JSON object, and the `match_documents` function will use the Postgres JSONB Containment operator `@>` to filter documents by the metadata field values you specify. See details on the [Postgres JSONB Containment operator](https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT) for more information. diff --git a/apps/docs/content/guides/ai/quickstarts/generate-text-embeddings.mdx b/apps/docs/content/guides/ai/quickstarts/generate-text-embeddings.mdx index f6921cebd91..ed7beaecbf8 100644 --- a/apps/docs/content/guides/ai/quickstarts/generate-text-embeddings.mdx +++ b/apps/docs/content/guides/ai/quickstarts/generate-text-embeddings.mdx @@ -9,7 +9,7 @@ This guide will walk you through how to generate high quality text embeddings in ## Build the Edge Function -Let's build an Edge Function that will accept an input string and generate an embedding for it. Edge Functions are server-side TypeScript HTTP endpoints that run on-demand closest to your users. +Build an Edge Function that accepts an input string and generates an embedding for it. Edge Functions are server-side TypeScript HTTP endpoints that run on-demand closest to your users. @@ -58,7 +58,7 @@ Let's build an Edge Function that will accept an input string and generate an em - Let's create a new inference session to be used in the lifetime of this function. Multiple requests can use the same inference session. + Create a new inference session to use for the lifetime of this function. Multiple requests can use the same inference session. Currently, only the `gte-small` (https://huggingface.co/Supabase/gte-small) text embedding model is supported in Supabase's Edge Runtime. diff --git a/apps/docs/content/guides/ai/rag-with-permissions.mdx b/apps/docs/content/guides/ai/rag-with-permissions.mdx index 08c8ca7b021..da34eb5353d 100644 --- a/apps/docs/content/guides/ai/rag-with-permissions.mdx +++ b/apps/docs/content/guides/ai/rag-with-permissions.mdx @@ -33,7 +33,7 @@ create table document_sections ( ); ``` -Notice how we record the `owner_id` on each document. Let's create an RLS policy that restricts access to `document_sections` based on whether or not they own the linked document: +Notice the record of `owner_id` on each document. Create an RLS policy that restricts access to `document_sections` based on whether or not they own the linked document: ```sql -- Grant the privileges the roles need @@ -85,7 +85,7 @@ Every app has its own unique requirements and may differ from the above example. ### Documents owned by multiple people -Instead of a one-to-many relationship between `users` and `documents`, you may require a many-to-many relationship so that multiple people can access the same document. Let's reimplement this using a join table: +Instead of a one-to-many relationship between `users` and `documents`, you may require a many-to-many relationship so that multiple people can access the same document. Reimplement this using a join table: ```sql create table document_owners ( @@ -112,7 +112,7 @@ Instead of directly querying the `documents` table, we query the join table. ### User and document data live outside of Supabase -You may have an existing system that stores users, documents, and their permissions in a separate database. Let's explore the scenario where this data exists in another Postgres database. We'll use a foreign data wrapper (FDW) to connect to the external DB from within your Supabase DB: +You may have an existing system that stores users, documents, and their permissions in a separate database. Consider the scenario where this data exists in another Postgres database. We'll use a foreign data wrapper (FDW) to connect to the external DB from within your Supabase DB: @@ -126,7 +126,7 @@ For data sources other than Postgres, see [Foreign Data Wrappers](/docs/guides/d -Let's assume your external DB contains a `users` and `documents` table like this: +Assume your external DB contains a `users` and `documents` table like this: ```sql create table public.users ( @@ -143,7 +143,7 @@ create table public.documents ( ); ``` -In your Supabase DB, let's create foreign tables that link to the above tables: +In your Supabase DB, create foreign tables that link to the above tables: ```sql create schema external; @@ -231,7 +231,7 @@ order by document_sections.embedding <#> embedding; {/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} -You might be tempted to discard RLS completely and simply filter by user within the `where` clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future. +You might be tempted to discard RLS completely and filter by user within the `where` clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future. diff --git a/apps/docs/content/guides/ai/vecs-python-client.mdx b/apps/docs/content/guides/ai/vecs-python-client.mdx index 16b08d2105e..ffd6389b56c 100644 --- a/apps/docs/content/guides/ai/vecs-python-client.mdx +++ b/apps/docs/content/guides/ai/vecs-python-client.mdx @@ -9,7 +9,7 @@ Supabase provides a Python client called [`vecs`](https://github.com/supabase/ve ## Quick start -Let's see how Vecs works using a local database. Make sure you have the Supabase CLI [installed](/docs/guides/cli#installation) on your machine. +To see how Vecs works, use a local database. Make sure you have the Supabase CLI [installed](/docs/guides/cli#installation) on your machine. ### Initialize your project diff --git a/apps/docs/content/guides/api/creating-routes.mdx b/apps/docs/content/guides/api/creating-routes.mdx index 7b065aabe7a..5f503a8e3aa 100644 --- a/apps/docs/content/guides/api/creating-routes.mdx +++ b/apps/docs/content/guides/api/creating-routes.mdx @@ -8,7 +8,7 @@ API routes are automatically created when you create Postgres Tables, Views, or ## Create a table -Let's create our first API route by creating a table called `todos` to store tasks. +Create your first API route by creating a table called `todos` to store tasks. This creates a corresponding route `todos` which can accept `GET`, `POST`, `PATCH`, & `DELETE` requests. - -Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys. - -In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/): - -- **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab. -- **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. - - +<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }} /> [Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses. @@ -87,7 +78,7 @@ Both of these routes require the key to be passed through an `apikey` header. You can interact with your API directly via HTTP requests, or you can use the client libraries which we provide. -Let's see how to make a request to the `todos` table which we created in the first step, +See how to make a request to the `todos` table which we created in the first step, using the API URL (`SUPABASE_URL`) and Key (`SUPABASE_PUBLISHABLE_KEY`) we provided: Log the full `error` object, not just `error.message`. + +## The recommended pattern + +Read `{ data, error }` from the response, check `error`, log the whole object, and return early. + +```ts +const { data, error } = await supabase.from('users').select() +if (error) { + console.error(error) + return +} +``` + +In the case of a permission-denied error, the response body will look like this: + +```json +{ + "error": { + "code": "42501", + "message": "permission denied for table users", + "details": null, + "hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.users TO anon;" + }, + "status": 401, + "statusText": "Unauthorized" +} +``` + +`postgrest-js` passes the body through verbatim, so `error.hint` is the exact string Postgres produced. Treat it as the answer the database is giving you, not as a suggestion to file away. + +## The `PostgrestError` fields, by usefulness + +Database calls (`select`, `insert`, `update`, `upsert`, `delete`, `rpc`) return a `PostgrestError` with four fields. Read them in roughly this order: + +| Field | Read it when | +| --------- | ------------------------------------------------------------------------------------------------------------------ | +| `hint` | Always check first. When Postgres includes one, it's the actionable fix (a `GRANT` to run, a column name, a type). | +| `code` | When branching in code. Codes are stable across versions; `message` text isn't. | +| `details` | When `hint` and `message` aren't enough. Often contains the offending value, key, or row. | +| `message` | As the human summary. Useful in UI strings, less useful for debugging. | + +A full list of PostgREST error codes is in the [Error Codes reference](/guides/api/rest/postgrest-error-codes). + +## Branch on `error.code`, not `error.message` + +`error.code` is more reliable than `error.message` for programmatic branching: messages change between Postgres and PostgREST versions, but codes are stable. + +```ts +const { data, error } = await supabase.from('users').select() +if (error) { + console.error(error) + if (error.code === '42501') { + // Permission denied. error.hint usually contains the GRANT to run. + } + return +} +``` + +## Errors from Auth, Storage, and Edge Functions + +The same rule applies across the SDK — log the whole error object — but the shape differs by client. + +### Auth + +`AuthError` exposes `error.code` (e.g. `'invalid_credentials'`, `'email_not_confirmed'`) and `error.status`. Branch on `code`; log the whole thing. + +```ts +const { data, error } = await supabase.auth.signInWithPassword({ + email: 'example@email.com', + password: 'example-password', +}) +if (error) { + console.error(error) + return +} +``` + +### Storage + +`StorageError` exposes `error.statusCode` (HTTP status as a string) and a structured `error` name (e.g. `'Duplicate'`, `'NotFound'`). + +```ts +const { data, error } = await supabase.storage + .from('avatars') + .upload('public/avatar1.png', avatarFile) +if (error) { + console.error(error) + return +} +``` + +### Edge Functions + +Functions errors arrive as one of three subclasses. Narrow with `instanceof`; for `FunctionsHttpError`, parse the body to get the function's own error payload. + +```ts +import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from '@supabase/supabase-js' + +const { data, error } = await supabase.functions.invoke('hello') +if (error instanceof FunctionsHttpError) { + console.error('Function error', await error.context.json()) +} else if (error) { + console.error(error) +} +``` + +### Realtime + +The `subscribe()` callback receives a `status` and, on failure, an `err` argument. Log the whole `err` — its `cause` often holds the underlying reason. + +```ts +supabase.channel('room1').subscribe((status, err) => { + if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') { + console.error(status, err) + } +}) +``` + +## Related + +- [PostgREST Error Codes](/guides/api/rest/postgrest-error-codes) +- [Automatic retries with `supabase-js`](/guides/api/automatic-retries-in-supabase-js) +- [Securing your API](/guides/api/securing-your-api) diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index c8d7f823f92..8d1276750bb 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -32,7 +32,7 @@ See the [Access control section](#access-control) for more details. -The Supabase team has received reports of user metadata being cached across unique anonymous users as a result of Next.js static page rendering. For the best user experience, utilize dynamic page rendering. +The Supabase team has received reports of user metadata being cached across unique anonymous users as a result of Next.js static page rendering. For the best user experience, use dynamic page rendering. diff --git a/apps/docs/content/guides/auth/auth-captcha.mdx b/apps/docs/content/guides/auth/auth-captcha.mdx index 6b5e2cc358e..11e6c6da73c 100644 --- a/apps/docs/content/guides/auth/auth-captcha.mdx +++ b/apps/docs/content/guides/auth/auth-captcha.mdx @@ -74,7 +74,7 @@ Now import the `HCaptcha` component from the `@hcaptcha/react-hcaptcha` library. import HCaptcha from '@hcaptcha/react-hcaptcha' ``` -Let's create a empty state to store our `captchaToken` +Create an empty state to store the `captchaToken` ```jsx const [captchaToken, setCaptchaToken] = useState() @@ -86,7 +86,7 @@ Now lets add the `HCaptcha` component to the JSX section of our code ``` -We will pass it the sitekey we copied from the hCaptcha website as a property along with a `onVerify` property which takes a callback function. This callback function will have a token as one of its properties. Let's set the token in the state using `setCaptchaToken` +Pass it the sitekey we copied from the hCaptcha website as a property along with a `onVerify` property which takes a callback function. This callback function will have a token as one of its properties. Set the token in the state using `setCaptchaToken` ```jsx ``` -We will pass it the sitekey we copied from the Cloudflare website as a property along with a `onSuccess` property which takes a callback function. This callback function will have a token as one of its properties. Let's set the token in the state using `setCaptchaToken`: +Pass it the sitekey we copied from the Cloudflare website as a property along with a `onSuccess` property which takes a callback function. This callback function will have a token as one of its properties. Set the token in the state using `setCaptchaToken`: ```jsx signInWithEmail() async { - final AuthResponse res = await supabase.auth.signinwithotp(email: 'valid.email@supabase.io'); + final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io'); } ``` @@ -145,10 +145,10 @@ That's it for the implicit flow. If you're using PKCE flow, edit the Magic Link [email template](/docs/guides/auth/auth-email-templates) to send a token hash: ```html -

Magic Link

+

Sign in to your account

-

Follow this link to login:

-

Log In

+

Use this link to sign in to your account:

+

Sign in

``` At the `/auth/confirm` endpoint, exchange the hash for the session: diff --git a/apps/docs/content/guides/auth/auth-hooks.mdx b/apps/docs/content/guides/auth/auth-hooks.mdx index 94471ff8803..b1259bc1d5a 100644 --- a/apps/docs/content/guides/auth/auth-hooks.mdx +++ b/apps/docs/content/guides/auth/auth-hooks.mdx @@ -148,6 +148,12 @@ Let us develop a Hook locally and then deploy it to the cloud. As a recap, here Edit `config.toml` to set up the Auth Hook locally. + + + The hook name used in the `config.toml` must correspond to one of the available Hooks listed above. For example, the Send SMS hook would be configured as: `[auth.hook.send_sms]` + + + - {[ - { - name: 'Custom Access Token', - description: 'Customize the access token issued by Supabase Auth', - href: '/guides/auth/auth-hooks/custom-access-token-hook', - }, - { - name: 'Send SMS', - description: 'Use a custom SMS provider to send authentication messages', - href: '/guides/auth/auth-hooks/send-sms-hook', - }, - { - name: 'Send Email', - description: 'Use a custom email provider to send authentication messages', - href: '/guides/auth/auth-hooks/send-email-hook', - }, - { - name: 'MFA Verification', - description: 'Add additional checks to the MFA verification flow', - href: '/guides/auth/auth-hooks/mfa-verification-hook', - }, - { - name: 'Password verification', - description: 'Add additional checks to the password verification flow', - href: '/guides/auth/auth-hooks/password-verification-hook', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + + Customize the access token issued by Supabase Auth + + +
+
+ + + Use a custom SMS provider to send authentication messages + + +
+
+ + + Use a custom email provider to send authentication messages + + +
+
+ + + Add additional checks to the MFA verification flow + + +
+
+ + + Add additional checks to the password verification flow + + +
diff --git a/apps/docs/content/guides/auth/auth-hooks/mfa-verification-hook.mdx b/apps/docs/content/guides/auth/auth-hooks/mfa-verification-hook.mdx index bbaf4cc924b..6e46c37860a 100644 --- a/apps/docs/content/guides/auth/auth-hooks/mfa-verification-hook.mdx +++ b/apps/docs/content/guides/auth/auth-hooks/mfa-verification-hook.mdx @@ -142,7 +142,7 @@ as $$ ( user_id, factor_id, - last_refreshed_at + last_failed_at ) values ( @@ -151,7 +151,7 @@ as $$ now() ) on conflict do update - set last_refreshed_at = now(); + set last_failed_at = now(); -- finally let Supabase Auth do the default behavior for a failed attempt return jsonb_build_object('decision', 'continue'); diff --git a/apps/docs/content/guides/auth/auth-mfa.mdx b/apps/docs/content/guides/auth/auth-mfa.mdx index 1cb3576ef18..542884c5aa8 100644 --- a/apps/docs/content/guides/auth/auth-mfa.mdx +++ b/apps/docs/content/guides/auth/auth-mfa.mdx @@ -127,7 +127,7 @@ Unenrolling a factor will downgrade the assurance level from `aal2` to `aal1` on ```tsx /** - * UnenrollMFA shows a simple table with the list of factors together with a button to unenroll. + * UnenrollMFA shows a table with the list of factors together with a button to unenroll. * When a user types in the factorId of the factor that they wish to unenroll and clicks unenroll * the corresponding factor will be unenrolled. */ diff --git a/apps/docs/content/guides/auth/auth-mfa/totp.mdx b/apps/docs/content/guides/auth/auth-mfa/totp.mdx index c645e15f681..c1869fbb097 100644 --- a/apps/docs/content/guides/auth/auth-mfa/totp.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/totp.mdx @@ -75,7 +75,7 @@ Below is an example that creates a new `EnrollMFA` component that illustrates th ```tsx /** - * EnrollMFA shows a simple enrollment dialog. When shown on screen it calls + * EnrollMFA shows an enrollment dialog. When shown on screen it calls * the `enroll` API. Each time a user clicks the Enable button it calls the * `challenge` and `verify` APIs to check if the code provided by the user is * valid. diff --git a/apps/docs/content/guides/auth/auth-smtp.mdx b/apps/docs/content/guides/auth/auth-smtp.mdx index 2e4d9fefd33..b43e261bda7 100644 --- a/apps/docs/content/guides/auth/auth-smtp.mdx +++ b/apps/docs/content/guides/auth/auth-smtp.mdx @@ -13,7 +13,7 @@ If you're using Supabase Auth with the following configuration: You will need to set up a custom SMTP server to handle the delivery of messages to your users. -To get you started and let you explore and set up email message templates for your application, Supabase provides a simple SMTP server for all projects. This server imposes a few important restrictions and is not meant for production use. +To get you started and let you explore and set up email message templates for your application, Supabase provides an SMTP server for all projects. This server imposes a few important restrictions and is not meant for production use. **Send messages only to pre-authorized addresses.** diff --git a/apps/docs/content/guides/auth/auth-web3.mdx b/apps/docs/content/guides/auth/auth-web3.mdx index 0a113aa4d63..63d00da8798 100644 --- a/apps/docs/content/guides/auth/auth-web3.mdx +++ b/apps/docs/content/guides/auth/auth-web3.mdx @@ -13,7 +13,7 @@ Supported Web3 wallets: ## How does it work? -Sign in with Web3 utilizes the [EIP 4361](https://eips.ethereum.org/EIPS/eip-4361) standard to authenticate wallet addresses off-chain. This standard is widely supported by the Ethereum and Solana ecosystems, making it the best choice for verifying wallet ownership. +Sign in with Web3 uses the [EIP 4361](https://eips.ethereum.org/EIPS/eip-4361) standard to authenticate wallet addresses off-chain. This standard is widely supported by the Ethereum and Solana ecosystems, making it the best choice for verifying wallet ownership. Authentication works by asking the Web3 wallet application to sign a predefined message with the user's wallet. This message is parsed both by the Web3 wallet application and Supabase Auth to verify its validity and purpose, before creating a user account or session. diff --git a/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx b/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx index ee7fc5d56dd..75e5841b52d 100644 --- a/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx +++ b/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx @@ -106,7 +106,7 @@ If you use [Multi-Factor Authentication](/docs/guides/auth/auth-mfa) with SSO, t A common use case with SSO is to use the UUID of the identity provider as the identifier for the organization the user belongs to -- frequently known as a tenant. By associating the identity provider's UUID with your tenants, you can use restrictive RLS policies to scope down actions and data that a user is able to access. -For example, let's say you have a table like: +For example, say you have a table like: ```sql create table organization_settings ( diff --git a/apps/docs/content/guides/auth/oauth-server.mdx b/apps/docs/content/guides/auth/oauth-server.mdx index f6b5c9736ce..31df7fae5fc 100644 --- a/apps/docs/content/guides/auth/oauth-server.mdx +++ b/apps/docs/content/guides/auth/oauth-server.mdx @@ -54,35 +54,34 @@ OAuth 2.1 Server works seamlessly with your existing Supabase Auth configuration To enable OAuth 2.1 Server in your project, follow these guides:
- {[ - { - name: 'Getting Started', - description: - 'Enable OAuth 2.1, configure your authorization endpoint, and register your first client.', - href: '/guides/auth/oauth-server/getting-started', - }, - { - name: 'OAuth Flows', - description: 'Detailed walkthrough of authorization code and refresh token flows.', - href: '/guides/auth/oauth-server/oauth-flows', - }, - { - name: 'MCP Authentication', - description: 'Authenticate AI agents and LLM tools using Model Context Protocol.', - href: '/guides/auth/oauth-server/mcp-authentication', - }, - { - name: 'Token Security & RLS', - description: 'Control data access with Row Level Security policies for OAuth clients.', - href: '/guides/auth/oauth-server/token-security', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + + Enable OAuth 2.1, configure your authorization endpoint, and register your first client. + + +
+
+ + + Detailed walkthrough of authorization code and refresh token flows. + + +
+
+ + + Authenticate AI agents and LLM tools using Model Context Protocol. + + +
+
+ + + Control data access with Row Level Security policies for OAuth clients. + + +
## Resources diff --git a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx index 1d40792deaf..a7f70ebcf6c 100644 --- a/apps/docs/content/guides/auth/oauth-server/getting-started.mdx +++ b/apps/docs/content/guides/auth/oauth-server/getting-started.mdx @@ -189,6 +189,8 @@ Here's how to build a minimal authorization page at your configured path (e.g., > +<$Partial path="auth_methods.mdx" /> + ```tsx // app/oauth/consent/page.tsx import { createServerClient } from '@supabase/ssr' @@ -221,11 +223,10 @@ export default async function ConsentPage({ ) // Check if user is authenticated - const { - data: { user }, - } = await supabase.auth.getUser() + const { data } = await supabase.auth.getClaims() + const claims = data?.claims - if (!user) { + if (!claims) { // Redirect to login, preserving authorization_id redirect(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`) } diff --git a/apps/docs/content/guides/auth/passkeys.mdx b/apps/docs/content/guides/auth/passkeys.mdx new file mode 100644 index 00000000000..1a732f7cfa7 --- /dev/null +++ b/apps/docs/content/guides/auth/passkeys.mdx @@ -0,0 +1,534 @@ +--- +id: 'passkeys' +title: 'Passkey authentication' +subtitle: 'Allow users to sign in with passkeys (WebAuthn)' +--- + +[Passkeys](https://fidoalliance.org/passkeys/) are a passwordless credential built on the [WebAuthn](https://www.w3.org/TR/webauthn-3/) standard. The user proves possession of a private key stored on their device (or password manager) using biometrics, a PIN, or a hardware security key. The matching public key is registered with Supabase Auth and used to verify future sign-ins. Passkeys are phishing-resistant and remove the need to manage shared secrets. + + + +Passkey support is experimental. The API may change without notice. You must explicitly opt-in when creating the Supabase client. See [Enable in the client](#enable-in-the-client). + + + + + +**Requires `@supabase/supabase-js` v2.105.0 and later, `supabase_flutter` v2.15.0 and later, or `supabase-swift` v2.48.0 and later.** Upgrade your client library to use passkey authentication. + + + +## How does it work? + +Each sign-in or registration is a WebAuthn ceremony with three steps: + +1. **Options**: the client requests a challenge from Supabase Auth. +2. **Ceremony**: the platform's passkey API (`navigator.credentials.create()` / `get()` on web, or a passkey plugin on iOS, Android, and macOS) prompts the user for biometrics or a security key. +3. **Verify**: the signed response is sent back to Supabase Auth, which validates the challenge and either stores the new credential or issues a session. + +Supabase Auth uses [discoverable credentials](https://www.w3.org/TR/webauthn-3/#discoverable-credential) for sign-in. The user does not need to provide an email, phone, or username — the authenticator resolves the account from the credential it stores. + +Registering a passkey requires an existing, confirmed, non-anonymous user. Sign-in works for any user that has previously registered a passkey, provided their email or phone is confirmed and the account is not banned. + +## Enable passkey authentication + +### Dashboard + +Open the [Passkeys settings](/dashboard/project/_/auth/passkeys) from the **Authentication → Passkeys** section of the Dashboard, turn on **Enable Passkey authentication**, and fill in the WebAuthn [relying party](https://www.w3.org/TR/webauthn-3/#relying-party) details: +{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} + +- **Relying Party Display Name**: a human-readable name for your application shown during the passkey prompt (for example, "My App"). +- **Relying Party ID**: the bare domain name for your application (for example, "example.com"). Do not include a scheme, port, or path. This determines which passkeys can be used. +- **Relying Party Origins**: comma-separated list of allowed origins (for example "https://example.com,https://app.example.com"). Up to 5 origins. + - HTTPS is required except for loopback addresses ("localhost", "127.0.0.1", "[::1]"). + - Each origin's hostname must match or be a subdomain of the Relying Party ID. + - Android native apps can use an app origin of the form `android:apk-key-hash:`. + +The dashboard pre-fills these from your project's Site URL and project name. Adjust them if your production app is served from a different domain. + + + +Passkeys are cryptographically bound to the Relying Party (RP) ID they were registered against. Changing the RP ID makes every existing passkey unusable for sign-in, and users will need to register a new one. Pick the RP ID carefully before users start enrolling, and keep it stable once they do. + + + +### CLI + +Add the following to `supabase/config.toml`: + +```toml +[auth.passkey] +enabled = true + +[auth.webauthn] +rp_display_name = "My App" +rp_id = "example.com" +rp_origins = ["https://example.com", "https://app.example.com"] +``` + +The `[auth.webauthn]` section is required when `auth.passkey.enabled` is `true`. + +### Management API + +You can also configure passkeys via the [Management API](/docs/reference/api/introduction): + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_ACCESS_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Read the current passkey configuration +curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + | jq '{passkey_enabled, webauthn_rp_id, webauthn_rp_display_name, webauthn_rp_origins}' + +# Enable passkeys and set the WebAuthn relying party +curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "passkey_enabled": true, + "webauthn_rp_display_name": "My App", + "webauthn_rp_id": "example.com", + "webauthn_rp_origins": "https://example.com,https://app.example.com" + }' +``` + +## Enable in the client + + + +Passkey support is currently experimental and requires explicit opt-in as the API may change without notice. + + + + + + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient(supabaseUrl, supabaseKey, { + auth: { + experimental: { passkey: true }, + }, +}) +``` + + + + +The Dart SDK does not require an opt-in flag — the methods are annotated `@experimental` so the analyzer surfaces them as preview API. The server independently rejects calls with `passkey_disabled` when the dashboard toggle is off. + +```dart +import 'package:supabase_flutter/supabase_flutter.dart'; + +await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseAnonKey, +); +final supabase = Supabase.instance.client; +``` + +Platform setup that the library cannot do for you (Associated Domains on iOS/macOS, Digital Asset Links on Android, and including the [`passkeys`](https://pub.dev/packages/passkeys) web SDK in `index.html` on web) is documented in the `supabase_flutter` package README. + + + + +The Swift SDK gates passkey support behind `@_spi(Experimental)`. Add this import to every file that uses passkey APIs: + +```swift +@_spi(Experimental) import Supabase +``` + +The `SupabaseClient` itself needs no extra configuration — the experimental SPI is enabled at the import site, not at client initialization. + +Platform setup the library cannot perform for you (Associated Domains entitlement and a relying-party server with HTTPS) must be configured in your Xcode project. Refer to [Apple's documentation on passkeys](https://developer.apple.com/documentation/authenticationservices/public-private_key_authentication/supporting_passkeys) for details. + + + + +## Register a passkey + +A user must be signed in before they can register a passkey. Typically, you call this from a security settings page, or directly after sign-up. + +`auth.registerPasskey()` runs the full WebAuthn ceremony. It fetches a challenge, invokes the platform passkey API, and verifies the response with Supabase Auth. + + + + +```ts +const { data, error } = await supabase.auth.registerPasskey() + +if (error) { + // User cancelled, browser doesn't support WebAuthn, or verification failed + console.error(error) +} else { + console.log('Registered passkey', data.id) +} +``` + + + + +```dart +try { + final Passkey passkey = await supabase.auth.registerPasskey(); + print('Registered passkey ${passkey.id}'); +} on AuthException catch (e) { + // The Supabase server rejected the credential. + print(e); +} catch (e) { + // User cancelled or the platform ceremony failed. + print(e); +} +``` + + + + +Available on iOS 16+, macOS 13+, and visionOS 1+. Requires `@_spi(Experimental) import Supabase`. + +```swift +do { + let passkey = try await supabase.auth.registerPasskey( + presentationAnchor: view.window! + ) + print("Registered passkey \(passkey.id)") +} catch { + // AuthError from the server, or user cancelled the native UI. + print(error) +} +``` + +For lower-level control (or on tvOS/watchOS), use `getPasskeyRegistrationOptions()` + `verifyPasskeyRegistration(challengeId:credentialResponse:)` from the [Auth Passkey](/docs/reference/swift/auth-passkey-api) reference. + + + + +The returned passkey contains the new credential's metadata: + +```ts +{ + id: string // UUID — use this to update or delete the passkey + friendly_name?: string // Derived from the authenticator's AAGUID + created_at: string +} +``` + +A friendly name is automatically derived from the authenticator's Authenticator Attestation GUID (AAGUID). For example, `iCloud Keychain`, `Google Password Manager`, `1Password`. Users can rename their passkey afterwards — see [Manage passkeys](#manage-passkeys). + +See the `registerPasskey` reference ([JavaScript](/docs/reference/javascript/auth-registerpasskey) · [Dart](/docs/reference/dart/auth-registerpasskey) · [Swift](/docs/reference/swift/auth-registerpasskey)) for the full API. + +## Sign in with a passkey + +`auth.signInWithPasskey()` runs the full discoverable-credential authentication ceremony. The user picks an account from the authenticator's UI — your app does not need to ask for an email or phone number upfront. + + + + +```ts +const { data, error } = await supabase.auth.signInWithPasskey() + +if (error) { + console.error(error) +} else { + // data.session and data.user are set; the client also dispatches a SIGNED_IN event + console.log('Signed in as', data.user?.email) +} +``` + + + + +```dart +try { + final AuthResponse res = await supabase.auth.signInWithPasskey(); + // res.session and res.user are set; the client also fires AuthChangeEvent.signedIn + print('Signed in as ${res.user?.email}'); +} on AuthException catch (e) { + print(e); +} +``` + + + + +Available on iOS 16+, macOS 13+, and visionOS 1+. Requires `@_spi(Experimental) import Supabase`. + +```swift +do { + let response = try await supabase.auth.signInWithPasskey( + presentationAnchor: view.window! + ) + // response.session and response.user are set; the client also fires a signedIn event. + print("Signed in as \(response.user?.email ?? "")") +} catch { + print(error) +} +``` + +For lower-level control (or on tvOS/watchOS), use `getPasskeyAuthenticationOptions()` + `verifyPasskeyAuthentication(challengeId:credentialResponse:)` from the [Auth Passkey](/docs/reference/swift/auth-passkey-api) reference. + + + + +See the `signInWithPasskey` reference ([JavaScript](/docs/reference/javascript/auth-signinwithpasskey) · [Dart](/docs/reference/dart/auth-signinwithpasskey) · [Swift](/docs/reference/swift/auth-signinwithpasskey)) for the full API. + +## Two-step API + +For native flows, custom UI, or full control over the WebAuthn ceremony, use the lower-level `auth.passkey` namespace. Each operation is split into "start" and "verify". + + + + +Registration: + +```ts +const { data: options } = await supabase.auth.passkey.startRegistration() +// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library) +const credential = await runRegistrationCeremony(options.options) +await supabase.auth.passkey.verifyRegistration({ + challengeId: options.challenge_id, + credential, +}) +``` + +Authentication: + +```ts +const { data: options } = await supabase.auth.passkey.startAuthentication() +// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library) +const credential = await runAuthenticationCeremony(options.options) +const { data } = await supabase.auth.passkey.verifyAuthentication({ + challengeId: options.challenge_id, + credential, +}) +``` + + + + +Registration: + +```dart +final registration = await supabase.auth.passkey.startRegistration(); +// Run the platform ceremony yourself (e.g. using a passkey plugin). +final Map credential = await runRegistrationCeremony( + registration.options, +); +final passkey = await supabase.auth.passkey.verifyRegistration( + challengeId: registration.challengeId, + credential: credential, +); +``` + +Authentication: + +```dart +final authentication = await supabase.auth.passkey.startAuthentication(); +// Run the platform ceremony yourself (e.g. using a passkey plugin). +final Map credential = await runAuthenticationCeremony( + authentication.options, +); +final AuthResponse res = await supabase.auth.passkey.verifyAuthentication( + challengeId: authentication.challengeId, + credential: credential, +); +``` + + + + +Requires `@_spi(Experimental) import Supabase`. Works on all Apple platforms (iOS, macOS, tvOS, watchOS, visionOS). + +Registration: + +```swift +let options = try await supabase.auth.getPasskeyRegistrationOptions() +// Run the platform authenticator yourself (e.g. via ASAuthorizationController). +let credential: AnyJSON = try await runRegistrationCeremony(options.options) +let passkey = try await supabase.auth.verifyPasskeyRegistration( + challengeId: options.challengeId, + credentialResponse: credential +) +``` + +Authentication: + +```swift +let options = try await supabase.auth.getPasskeyAuthenticationOptions() +// Run the platform authenticator yourself (e.g. via ASAuthorizationController). +let credential: AnyJSON = try await runAuthenticationCeremony(options.options) +let response = try await supabase.auth.verifyPasskeyAuthentication( + challengeId: options.challengeId, + credentialResponse: credential +) +``` + + + + +The `options` field returned from the start methods matches the [WebAuthn `PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions) and [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions) shapes (with `ArrayBuffer` fields encoded as base64url). + +See the `auth.passkey` reference ([JavaScript](/docs/reference/javascript/auth-passkey-api) · [Dart](/docs/reference/dart/auth-passkey-api) · [Swift](/docs/reference/swift/auth-passkey-api)) for the full API. + +## Manage passkeys + +List, rename, and delete the current user's passkeys: + + + + +```ts +// List +const { data: passkeys } = await supabase.auth.passkey.list() +// [{ id, friendly_name, created_at, last_used_at? }, ...] + +// Rename +await supabase.auth.passkey.update({ + passkeyId: passkeys[0].id, + friendlyName: 'Work laptop', +}) + +// Delete +await supabase.auth.passkey.delete({ passkeyId: passkeys[0].id }) +``` + + + + +```dart +// List +final List passkeys = await supabase.auth.passkey.list(); + +// Rename +await supabase.auth.passkey.update( + passkeyId: passkeys.first.id, + friendlyName: 'Work laptop', +); + +// Delete +await supabase.auth.passkey.delete(passkeyId: passkeys.first.id); +``` + + + + +Requires `@_spi(Experimental) import Supabase`. + +```swift +// List +let passkeys: [PasskeyListItem] = try await supabase.auth.listPasskeys() + +// Rename +let updated = try await supabase.auth.renamePasskey( + id: passkeys.first!.id, + friendlyName: "Work laptop" +) + +// Delete +try await supabase.auth.deletePasskey(id: passkeys.first!.id) +``` + + + + +`friendlyName` is limited to 120 characters. `lastUsedAt` is updated each time the passkey is used to sign in. + +See the `auth.passkey` reference ([JavaScript](/docs/reference/javascript/auth-passkey-api) · [Dart](/docs/reference/dart/auth-passkey-api) · [Swift](/docs/reference/swift/auth-passkey-api)) for the full API. + +## Admin API + +Server-side admin endpoints let you inspect and revoke a user's passkeys. These require the project's secret key and must only be called from a trusted server. + + + + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient(supabaseUrl, supabaseSecretKey, { + auth: { experimental: { passkey: true } }, +}) + +const { data } = await supabase.auth.admin.passkey.listPasskeys({ userId }) + +await supabase.auth.admin.passkey.deletePasskey({ userId, passkeyId }) +``` + + + + +```dart +final supabase = SupabaseClient(supabaseUrl, secretKey); + +final List passkeys = await supabase.auth.admin.passkey.listPasskeys( + userId: userId, +); + +await supabase.auth.admin.passkey.deletePasskey( + userId: userId, + passkeyId: passkeyId, +); +``` + + + + +See the `auth.admin.passkey` reference ([JavaScript](/docs/reference/javascript/auth-admin-passkey-api) · [Dart](/docs/reference/dart/auth-admin-passkey-api)) for the full API. The Swift SDK does not expose admin passkey methods. + +## Error codes + +| Code | Meaning | +| ------------------------------- | -------------------------------------------------------------------------------- | +| `passkey_disabled` | Passkey sign-in is not enabled for this project. | +| `too_many_passkeys` | The user has reached the maximum number of passkeys allowed per account. | +| `webauthn_credential_exists` | This authenticator has already been registered to the account. | +| `webauthn_credential_not_found` | The credential in the assertion is not registered with Supabase Auth. | +| `webauthn_challenge_not_found` | The challenge ID is unknown or has already been consumed. | +| `webauthn_challenge_expired` | The challenge expired before the client returned a credential. | +| `webauthn_verification_failed` | The signature, attestation, or assertion did not validate against the challenge. | + +In addition, `signInWithPasskey()` returns the usual sign-in failure modes: `email_not_confirmed`, `phone_not_confirmed`, and `user_banned`. + +## Limitations + +- SSO users cannot register passkeys. +- Anonymous users cannot register passkeys — link an email or phone first. diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index 4ad2eb6aafb..4548d785ba8 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -1111,6 +1111,8 @@ If you want users to confirm their phone number on signup, you need to set up an +<$Partial path="cost_warning.mdx" /> + ### Signing up with a phone number and password To sign up the user, call [`signUp()`](/docs/reference/javascript/auth-signup) with their phone number and password: diff --git a/apps/docs/content/guides/auth/phone-login.mdx b/apps/docs/content/guides/auth/phone-login.mdx index 8d3399d9c91..7f5615ff944 100644 --- a/apps/docs/content/guides/auth/phone-login.mdx +++ b/apps/docs/content/guides/auth/phone-login.mdx @@ -19,7 +19,7 @@ Phone OTP login can: - Increase security by reducing the risk of password-related security breaches - Reduce support burden of dealing with password resets and other password-related flows - +<$Partial path="cost_warning.mdx" /> ## Enabling phone login diff --git a/apps/docs/content/guides/auth/quickstarts/react.mdx b/apps/docs/content/guides/auth/quickstarts/react.mdx index 6d498228376..7c99938fecd 100644 --- a/apps/docs/content/guides/auth/quickstarts/react.mdx +++ b/apps/docs/content/guides/auth/quickstarts/react.mdx @@ -68,8 +68,6 @@ hideToc: true - - diff --git a/apps/docs/content/guides/auth/server-side.mdx b/apps/docs/content/guides/auth/server-side.mdx index d0fdc6cd108..81ae770cd4a 100644 --- a/apps/docs/content/guides/auth/server-side.mdx +++ b/apps/docs/content/guides/auth/server-side.mdx @@ -15,33 +15,21 @@ Make sure to use the PKCE flow instructions where those differ from the implicit ## `@supabase/ssr` -We have developed an [`@supabase/ssr`](https://www.npmjs.com/package/@supabase/ssr) package to make setting up the Supabase client as simple as possible. This package is currently in beta. Adoption is recommended but be aware that the API is still unstable and may have breaking changes in the future. +We have developed an [`@supabase/ssr`](https://www.npmjs.com/package/@supabase/ssr) package for setting up the Supabase client. This package is currently in beta. Adoption is recommended but be aware that the API is still unstable and may have breaking changes in the future. ## Framework quickstarts
- {[ - { - title: 'Next.js', - href: '/guides/auth/server-side/nextjs', - description: - 'Automatically configure Supabase in Next.js to use cookies, making your user and their session available on the client and server.', - icon: '/docs/img/icons/nextjs-icon', - }, - { - title: 'SvelteKit', - href: '/guides/auth/server-side/sveltekit', - description: - 'Automatically configure Supabase in SvelteKit to use cookies, making your user and their session available on the client and server.', - icon: '/docs/img/icons/svelte-icon', - }, - ].map((item) => { - return ( - - - {item.description} - - - ) - })} + + + Automatically configure Supabase in Next.js to use cookies, making your user and their session + available on the client and server. + + + + + Automatically configure Supabase in SvelteKit to use cookies, making your user and their + session available on the client and server. + +
diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx index 294d672d068..92ce3e4f2d2 100644 --- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx +++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx @@ -172,6 +172,8 @@ You need setup code to configure a Supabase client to use cookies. Once you have Use the browser client in code that runs on the browser, and the server client in code that runs on the server. +<$Partial path="auth_methods.mdx" /> + + +<$Partial path="auth_methods.mdx" /> +
<$CodeTabs> <$CodeSample path="/auth/nextjs/proxy.ts" meta="name=proxy.ts" language="typescript" /> @@ -291,6 +297,8 @@ Set up server-side hooks in `src/hooks.server.ts`. The hooks: - Check user authentication. - Guard protected pages. +<$Partial path="auth_methods.mdx" /> + <$CodeSample path="/auth/sveltekit/src/hooks.server.ts" meta="name=src/hooks.server.ts" @@ -609,7 +617,7 @@ export default defineEventHandler(async (event) => { } ) - await supabase.auth.getUser() + await supabase.auth.getClaims() return { ok: true } }) @@ -843,6 +851,8 @@ language="typescript" You can now use this middleware in your Hono application to create a server Supabase client that can be used to make authenticated requests. +<$Partial path="auth_methods.mdx" /> + <$CodeSample path="/auth/hono/src/index.tsx" meta="name=src/index.tsx" diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index ce6cade5649..2f0ef6d8bb1 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -280,7 +280,7 @@ const { data, error } = await supabase.auth.signInWithOAuth({ ### Google pre-built [#google-pre-built] -Most web apps and websites can utilize Google's [personalized sign-in buttons](https://developers.google.com/identity/gsi/web/guides/personalized-button), [One Tap](https://developers.google.com/identity/gsi/web/guides/features) or [automatic sign-in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) for the best user experience. +Most web apps and websites can use Google's [personalized sign-in buttons](https://developers.google.com/identity/gsi/web/guides/personalized-button), [One Tap](https://developers.google.com/identity/gsi/web/guides/features) or [automatic sign-in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) for the best user experience. 1. Load the Google client library in your app by including the third-party script: diff --git a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx index 12f1138502f..e80d89cb7aa 100644 --- a/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-linkedin.mdx @@ -177,7 +177,7 @@ suspend fun signOut() { ## LinkedIn Open ID Connect (OIDC) -We will be replacing the _LinkedIn_ provider with a new _LinkedIn (OIDC)_ provider to support recent changes to the LinkedIn [OAuth APIs](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS1). The new provider utilizes the [Open ID Connect standard](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#validating-id-tokens). In view of this change, we have disabled edits on the _LinkedIn_ provider and will be removing it effective 4th January 2024. Developers with LinkedIn OAuth Applications created prior to 1st August 2023 should create a new OAuth application [via the steps outlined above](/docs/guides/auth/social-login/auth-linkedin#create-a-linkedin-oauth-app) and migrate their credentials from the _LinkedIn_ provider to the _LinkedIn (OIDC)_ provider. Alternatively, you can also head to the `Products` section and add the newly release`Sign In with LinkedIn using OpenID Connect` to your existing OAuth application. +We are replacing the _LinkedIn_ provider with a new _LinkedIn (OIDC)_ provider to support recent changes to the LinkedIn [OAuth APIs](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS1). The new provider uses the [Open ID Connect standard](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#validating-id-tokens). In view of this change, we have disabled edits on the _LinkedIn_ provider and will be removing it effective 4th January 2024. Developers with LinkedIn OAuth Applications created prior to 1st August 2023 should create a new OAuth application [via the steps outlined above](/docs/guides/auth/social-login/auth-linkedin#create-a-linkedin-oauth-app) and migrate their credentials from the _LinkedIn_ provider to the _LinkedIn (OIDC)_ provider. Alternatively, you can also head to the `Products` section and add the newly release`Sign In with LinkedIn using OpenID Connect` to your existing OAuth application. Developers using the Supabase CLI to test their LinkedIn OAuth application should also update their `config.toml` to make use of the new provider: diff --git a/apps/docs/content/guides/auth/third-party/clerk.mdx b/apps/docs/content/guides/auth/third-party/clerk.mdx index 1358f8421db..25cfda14e36 100644 --- a/apps/docs/content/guides/auth/third-party/clerk.mdx +++ b/apps/docs/content/guides/auth/third-party/clerk.mdx @@ -77,7 +77,7 @@ import Supabase let supabase = SupabaseClient( supabaseURL: URL(string: "https://project-ref.supabase.io")!, - supabaseKey: "supabase.anon.key", + supabaseKey: "supabase.publishable.key", options: SupabaseClientOptions( auth: SupabaseClientOptions.AuthOptions( accessToken: { diff --git a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx index b1e9f4380cf..fb9d28422b0 100644 --- a/apps/docs/content/guides/auth/third-party/firebase-auth.mdx +++ b/apps/docs/content/guides/auth/third-party/firebase-auth.mdx @@ -338,7 +338,7 @@ const { initializeApp } = require('firebase-admin/app'); const { getAuth } = require('firebase-admin/auth'); initializeApp(); -async function setRoleCustomClaim() => { +async function setRoleCustomClaim() { let nextPageToken = undefined do { diff --git a/apps/docs/content/guides/cli.mdx b/apps/docs/content/guides/cli.mdx index 5c81c2414bf..65484bc4c96 100644 --- a/apps/docs/content/guides/cli.mdx +++ b/apps/docs/content/guides/cli.mdx @@ -6,32 +6,26 @@ 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, see the [installation guide](/docs/guides/cli/getting-started#installing-the-supabase-cli). +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
- {[ - { - name: 'Supabase CLI', - description: - 'The Supabase CLI provides tools to develop manage your Supabase projects from your local machine.', - href: 'https://github.com/supabase/cli', - }, - { - name: 'GitHub Action', - description: ' A GitHub action for interacting with your Supabase projects using the CLI.', - href: 'https://github.com/supabase/setup-cli', - }, - ].map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + 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/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index 9f811ac302c..20e48681bd6 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres.mdx @@ -1,7 +1,7 @@ --- title: 'Connect to your database' description: 'Connect to Postgres from your frontend, backend, or serverless environment' -subtitle: 'Supabase provides multiple methods to connect to your Postgres database, whether you’re working on the frontend, backend, or utilizing serverless functions.' +subtitle: 'Supabase provides multiple methods to connect to your Postgres database, whether you’re working on the frontend, backend, or using serverless functions.' --- ## How to connect to your Postgres databases @@ -10,9 +10,24 @@ How you connect to your database depends on where you're connecting from: - For frontend applications, use the [Data API](#data-apis-and-client-libraries) - For Postgres clients, use a connection string - - For single sessions (for example, database GUIs) or Postgres native commands (for example, using client applications like [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html), [migrations](/docs/guides/deployment/database-migrations), [backup-restore](/docs/guides/platform/migrating-within-supabase/backup-restore), or specifying connections for [replication](/docs/guides/database/postgres/setup-replication-external)) use the [direct connection string](#direct-connection) if your environment supports IPv6. IPv4 available as [Add-on](/docs/guides/platform/ipv4-address). - - For application traffic from persistent clients, and support for both IPv4 and IPv6, use [pooler session mode](#pooler-session-mode) - - For application traffic from temporary clients (for example, serverless or edge functions) use [pooler transaction mode](#pooler-transaction-mode) + - **Use the [direct connection string](#direct-connection) for single sessions or Postgres native commands**. For example, database GUIs, client applications like [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html), [migrations](/docs/guides/deployment/database-migrations), [backup-restore](/docs/guides/platform/migrating-within-supabase/backup-restore), or specifying connections for [replication](/docs/guides/database/postgres/setup-replication-external). The direct endpoint is on IPv6, or on IPv4 if the project has the [IPv4 add-on](/docs/guides/platform/ipv4-address). + - **Use [pooler session mode](#pooler-session-mode)** for application traffic from persistent clients on IPv4-only networks, + - **Use [pooler transaction mode](#pooler-transaction-mode)** for application traffic from temporary clients (for example, serverless or edge functions). + +The table below summarizes each mode, its host and port, IP version support per project tier, and what it's best used for: + +| Mode | Host:Port | Free | Paid | Paid + IPv4 add-on | Best for | +| ----------------------------------------------- | --------------------------------------- | ---- | ---- | ------------------ | ------------------------------------------ | +| Direct connection | `db.[project-id].supabase.co:5432` | IPv6 | IPv6 | IPv4 | Migrations, `pg_dump`, long-lived backend | +| Shared pooler (Supavisor) - session mode | `aws-[region].pooler.supabase.com:5432` | IPv4 | IPv4 | IPv4 | Persistent backend on IPv4-only networks | +| Shared pooler (Supavisor) - transaction mode | `aws-[region].pooler.supabase.com:6543` | IPv4 | IPv4 | IPv4 | Serverless and edge functions | +| Dedicated pooler (PgBouncer) - transaction mode | `db.[project-id].supabase.co:6543` | - | IPv6 | IPv4 | High-performance app traffic on paid tiers | + + + +The IPv4 add-on is not dual-stack: enabling it swaps the project's IPv6 (AAAA) DNS record for an IPv4 (A) record, so the project endpoint becomes reachable only over IPv4. + + ## Quickstarts @@ -67,7 +82,7 @@ The direct connection string connects directly to your Postgres instance. It is -Direct connections use IPv6 by default. If your environment doesn't support IPv6, use [Supavisor session mode](#supavisor-session-mode) or get the [IPv4 add-on](/docs/guides/platform/ipv4-address). +Direct connections are on IPv6, or on IPv4 if the project has the [IPv4 add-on](/docs/guides/platform/ipv4-address). If your network is IPv4-only and you don't have the add-on, use [pooler session mode](#pooler-session-mode) instead. @@ -81,23 +96,23 @@ Get your project's direct connection string from your project dashboard by click ## Poolers -Every Supabase project includes a connection pooler. This is ideal for persistent servers when IPv6 is not supported. +Supabase offers two poolers. The **Shared Pooler** ([Supavisor](https://github.com/supabase/supavisor)) is multi-tenant, available on every project, and IPv4-only. The **Dedicated Pooler** ([PgBouncer](https://www.pgbouncer.org/)) is available on paid plans and co-located with your Postgres instance; like the direct connection, it is on IPv6, or on IPv4 if the project has the [IPv4 add-on](/docs/guides/platform/ipv4-address). ### Pooler session mode -The session mode connection string connects to your Postgres instance via a proxy. This is only recommended as an alternative to a Direct Connection, when connecting via an IPv4 network. +The session mode connection string connects to your Postgres instance via the Shared Pooler (Supavisor). This is only recommended as an alternative to a Direct Connection when connecting from an IPv4-only network. The connection string looks like this: ``` -postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres +postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-[REGION].pooler.supabase.com:5432/postgres ``` Get your project's Session pooler connection string from your project dashboard by clicking [Connect](/dashboard/project/_?showConnect=true&method=session). ### Pooler transaction mode -The transaction mode connection string connects to your Postgres instance via a proxy which serves as a connection pooler. This is ideal for serverless or edge functions, which require many transient connections. +The transaction mode connection string connects to your Postgres instance via the Shared Pooler (Supavisor) in transaction-pooling mode. This is ideal for serverless or edge functions, which require many transient connections. @@ -108,14 +123,20 @@ Transaction mode does not support [prepared statements](https://postgresql.org/d The connection string looks like this: ``` -postgres://postgres:[YOUR-PASSWORD]@db.abcdefghijklmnopqrst.supabase.co:6543/postgres +postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-[REGION].pooler.supabase.com:6543/postgres ``` Get your project's Transaction pooler connection string from your project dashboard by clicking [Connect](/dashboard/project/_?showConnect=true&method=transaction). ## Dedicated pooler -For paying customers, we provision a Dedicated Pooler ([PgBouncer](https://www.pgbouncer.org/)) that's co-located with your Postgres database. This will require you to connect with IPv6 or, if that's not an option, you can use the [IPv4 add-on](/docs/guides/platform/ipv4-address). +For paying customers, we provision a Dedicated Pooler ([PgBouncer](https://www.pgbouncer.org/)) that's co-located with your Postgres database. The Dedicated Pooler runs in transaction mode only - for session mode, use the [Shared Pooler](#pooler-session-mode). It is reachable over IPv6, or over IPv4 if the project has the [IPv4 add-on](/docs/guides/platform/ipv4-address). + +The connection string looks like this: + +``` +postgres://postgres:[YOUR-PASSWORD]@db.abcdefghijklmnopqrst.supabase.co:6543/postgres +``` The Dedicated Pooler ensures best performance and latency, while using up more of your project's compute resources. If your network supports IPv6 or you have the IPv4 add-on, we encourage you to use the Dedicated Pooler over the Shared Pooler. @@ -131,11 +152,11 @@ You can use an application-side pooler or a server-side pooler (Supabase automat Application-side poolers are built into connection libraries and API servers, such as Prisma, SQLAlchemy, and PostgREST. They maintain several active connections with Postgres or a server-side pooler, reducing the overhead of establishing connections between queries. When deploying to static architecture, such as long-standing containers or VMs, application-side poolers are satisfactory on their own. -### Serverside poolers +### Server-side poolers Postgres connections are like a WebSocket. Once established, they are preserved until the client (application server) disconnects. A server might only make a single 10 ms query, but needlessly reserve its database connection for seconds or longer. -Serverside-poolers, such as Supabase's [Supavisor](https://github.com/supabase/supavisor) in transaction mode, sit between clients and the database and can be thought of as load balancers for Postgres connections. +Server-side poolers, such as Supabase's [Supavisor](https://github.com/supabase/supavisor) in transaction mode, sit between clients and the database and can be thought of as load balancers for Postgres connections. New migration files trigger migrations on the preview instance.
- {[ - { - title: 'supabase-js', - hasLightIcon: true, - href: 'https://supabase.link/nextjs-with-supabase-starter', - description: 'A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript and Tailwind CSS.' - }, - /* { TODO: Link the correct next.js template that uses drizzle ORM with supabase database. - hasLightIcon: true, - href: 'https://supabase.link/nextjs-supabase-drizzle', - description: "Simple Next.js template that uses Supabase as the database and Drizzle as the ORM.", - },*/ - { - title: 'Kysely', - hasLightIcon: true, - href: 'https://supabase.link/nextjs-supabase-kysely', - description: 'Simple Next.js template that uses Supabase as the database and Kysely as the query builder.', - }, - /* { TODO: figure out how to get around Prisma accelerate requirement... - title: 'Prisma', - hasLightIcon: true, - href: 'https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fstorage%2Fpostgres-prisma&project-name=postgres-prisma&repository-name=postgres-prisma&demo-title=Vercel%20Postgres%20%2B%20Prisma%20Next.js%20Starter&demo-description=Simple%20Next.js%20template%20that%20uses%20Vercel%20Postgres%20as%20the%20database%20and%20Prisma%20as%20the%20ORM.&demo-url=https%3A%2F%2Fpostgres-prisma.vercel.app%2F&demo-image=https%3A%2F%2Fpostgres-prisma.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6', - description: 'Simple Next.js template that uses Vercel Postgres as the database and Prisma as the ORM.', - } */ - ].map((resource) => { - return ( - - - {resource.description} - - - ) - -})} - + + + A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript + and Tailwind CSS. + + + + + Basic Next.js template that uses Supabase as the database and Kysely as the query builder. + +
@@ -75,9 +54,9 @@ POSTGRES_URL="postgres://postgres.cfcxynqnhdybqtbhjemm:[YOUR-PASSWORD]@aws-0-ap- ```ts lib/drizzle.ts -import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core' -import { InferSelectModel, InferInsertModel } from 'drizzle-orm' import { sql } from '@vercel/postgres' +import { InferInsertModel, InferSelectModel } from 'drizzle-orm' +import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/vercel-postgres' export const UsersTable = pgTable( @@ -107,8 +86,8 @@ export const db = drizzle(sql) ```ts lib/kysely.ts -import { Generated, ColumnType } from 'kysely' import { createKysely } from '@vercel/postgres-kysely' +import { ColumnType, Generated } from 'kysely' interface UserTable { // Columns that are generated by the database should be marked diff --git a/apps/docs/content/guides/database/connection-management.mdx b/apps/docs/content/guides/database/connection-management.mdx index eab783c2928..48b2c4f1b7b 100644 --- a/apps/docs/content/guides/database/connection-management.mdx +++ b/apps/docs/content/guides/database/connection-management.mdx @@ -51,7 +51,7 @@ For more details on using these monitoring charts, see the [Reports guide](/docs #### Grafana Dashboard -Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/platform/metrics). +Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/telemetry/metrics). Its "Client Connections" graph displays connections for both Supavisor and Postgres ![client connection graph](/docs/img/database/grafana-connections.png) diff --git a/apps/docs/content/guides/database/custom-postgres-config.mdx b/apps/docs/content/guides/database/custom-postgres-config.mdx index 4a2c30b7db6..51c533bf808 100644 --- a/apps/docs/content/guides/database/custom-postgres-config.mdx +++ b/apps/docs/content/guides/database/custom-postgres-config.mdx @@ -212,7 +212,7 @@ You can also pass the `--no-restart` flag to attempt a reload-only apply. If the Postgres requires several parameters to be synchronized between the Primary cluster and [Read Replicas](/docs/guides/platform/read-replicas). -By default, Supabase ensures that this propagation is executed correctly. However, if the `--no-restart` behavior is used in conjunction with parameters that cannot be reloaded without a restart, the user is responsible for ensuring that both the primaries and the read replicas get restarted in a timely manner to ensure a stable running state. Leaving the configuration updated, but not utilized (via a restart) in such a case can result in read replica failure if the primary, or a read replica, restarts in isolation (e.g. due to an out-of-memory event, or hardware failure). +By default, Supabase ensures that this propagation is executed correctly. However, if the `--no-restart` behavior is used in conjunction with parameters that cannot be reloaded without a restart, the user is responsible for ensuring that both the primaries and the read replicas get restarted in a timely manner to ensure a stable running state. Leaving the configuration updated, but not used (via a restart) in such a case can result in read replica failure if the primary, or a read replica, restarts in isolation (e.g. due to an out-of-memory event, or hardware failure). diff --git a/apps/docs/content/guides/database/debugging-performance.mdx b/apps/docs/content/guides/database/debugging-performance.mdx index a2f349d7547..f333c94bb56 100644 --- a/apps/docs/content/guides/database/debugging-performance.mdx +++ b/apps/docs/content/guides/database/debugging-performance.mdx @@ -44,7 +44,7 @@ create table instruments ( name text ); -insert into books +insert into instruments (id, name) values (1, 'violin'), diff --git a/apps/docs/content/guides/database/drizzle.mdx b/apps/docs/content/guides/database/drizzle.mdx index 88a02df030e..c9a58a1554e 100644 --- a/apps/docs/content/guides/database/drizzle.mdx +++ b/apps/docs/content/guides/database/drizzle.mdx @@ -80,16 +80,19 @@ If you plan on solely using Drizzle instead of the Supabase Data API (PostgREST) import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' - let connectionString = process.env.DATABASE_URL - if (host.includes('postgres:postgres@supabase_db_')) { - const url = URL.parse(host)! + const databaseUrl = process.env.DATABASE_URL + if (!databaseUrl) throw new Error('DATABASE_URL is not set') + + let connectionString = databaseUrl + if (connectionString.includes('postgres:postgres@supabase_db_')) { + const url = new URL(connectionString) url.hostname = url.hostname.split('_')[1] connectionString = url.href } // Disable prefetch as it is not supported for "Transaction" pool mode export const client = postgres(connectionString, { prepare: false }) - export const db = drizzle(client); + export const db = drizzle(client) ```
diff --git a/apps/docs/content/guides/database/extensions.mdx b/apps/docs/content/guides/database/extensions.mdx index e7e090c6ad4..0c4dee671b3 100644 --- a/apps/docs/content/guides/database/extensions.mdx +++ b/apps/docs/content/guides/database/extensions.mdx @@ -44,7 +44,7 @@ To disable an extension call `drop extension`. -Most extensions are installed under the `extensions` schema, which is accessible to public by default. To avoid namespace pollution, we do not recommend creating other entities in the `extensions` schema. +Most extensions are installed under the `extensions` schema, which is accessible to `public` by default. To avoid namespace pollution, we do not recommend creating other entities in the `extensions` schema. If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension. diff --git a/apps/docs/content/guides/database/extensions/http.mdx b/apps/docs/content/guides/database/extensions/http.mdx index ed4052e62e0..5cd72c01538 100644 --- a/apps/docs/content/guides/database/extensions/http.mdx +++ b/apps/docs/content/guides/database/extensions/http.mdx @@ -20,7 +20,7 @@ The `http` extension allows you to call RESTful endpoints within Postgres. ## Overview -Let's cover some basic concepts: +This section covers basic concepts: - REST: stands for REpresentational State Transfer. It's a way to request data from external services. - RESTful APIs are servers which accept HTTP "calls". The calls are typically: @@ -88,7 +88,7 @@ A successful call to a web URL from the `http` extension returns a record with t ## Examples -### Simple `GET` example +### Basic `GET` example [#simple-get-example] ```sql select @@ -97,7 +97,7 @@ from extensions.http_get('https://jsonplaceholder.typicode.com/todos/1'); ``` -### Simple `POST` example +### Basic `POST` example [#simple-post-example] ```sql select diff --git a/apps/docs/content/guides/database/extensions/hypopg.mdx b/apps/docs/content/guides/database/extensions/hypopg.mdx index 3a3875ef139..6bea2618227 100644 --- a/apps/docs/content/guides/database/extensions/hypopg.mdx +++ b/apps/docs/content/guides/database/extensions/hypopg.mdx @@ -45,7 +45,7 @@ It's good practice to create the extension within a separate schema (like `exten ### Speeding up a query -Given the following table and a simple query to select from the table by `id`: +Given the following table and a basic query to select from the table by `id`: {/* prettier-ignore */} ```sql diff --git a/apps/docs/content/guides/database/extensions/pg_graphql.mdx b/apps/docs/content/guides/database/extensions/pg_graphql.mdx index 389dc09ddd6..ce5845773f0 100644 --- a/apps/docs/content/guides/database/extensions/pg_graphql.mdx +++ b/apps/docs/content/guides/database/extensions/pg_graphql.mdx @@ -90,7 +90,7 @@ returning the JSON "edges": [ { "node": { - "id": 1 + "id": 1, "name": "My Blog" } } diff --git a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx index 17e8782a234..291f9c73a6f 100644 --- a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx +++ b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx @@ -52,7 +52,7 @@ explain select * from book; (1 row) ``` -Now we can choose a `statement_cost_filter` value between the total cost for the single select (2.49) and the whole table select (135.0) so one statement will succeed and one will fail. +Now we can choose a `statement_cost_limit` value between the total cost for the single select (2.49) and the whole table select (135.0) so one statement will succeed and one will fail. {/* prettier-ignore */} ```sql diff --git a/apps/docs/content/guides/database/extensions/pgaudit.mdx b/apps/docs/content/guides/database/extensions/pgaudit.mdx index 05fa6156868..48c482aa12b 100644 --- a/apps/docs/content/guides/database/extensions/pgaudit.mdx +++ b/apps/docs/content/guides/database/extensions/pgaudit.mdx @@ -113,7 +113,7 @@ set pgaudit.log = 'none'; ### User logging -There are some cases where you may want to monitor a database user's actions. For instance, let's say you connected your database to [Zapier](/partners/integrations/zapier) and created a custom role for it to use: +There are some cases where you may want to monitor a database user's actions. For instance, say you connected your database to [Zapier](/partners/integrations/zapier) and created a custom role for it to use: ```sql create user "zapier" with password ''; @@ -391,5 +391,5 @@ PGAudit's [official documentation](https://www.pgaudit.org) focuses on system an - [Official `PGAudit` documentation](https://www.pgaudit.org) - [Database Function Logging](/docs/guides/database/functions#general-logging) -- [Supabase Logging](/docs/guides/platform/logs) +- [Supabase Logging](/docs/guides/telemetry/logs) - [Self-Hosting Logs](/docs/reference/self-hosting-analytics/introduction) diff --git a/apps/docs/content/guides/database/extensions/pgjwt.mdx b/apps/docs/content/guides/database/extensions/pgjwt.mdx index fb0283e5dce..a77f55297df 100644 --- a/apps/docs/content/guides/database/extensions/pgjwt.mdx +++ b/apps/docs/content/guides/database/extensions/pgjwt.mdx @@ -55,8 +55,8 @@ It's good practice to create the extension within a separate schema (like `exten ## API -- [`sign(payload json, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containing _payload_ with _secret_ using _algorithm_. -- [`verify(token text, secret text, algorithm text default 'HSA256')`](https://github.com/michelp/pgjwt#usage): Decodes a JWT _token_ that was signed with _secret_ using _algorithm_. +- [`sign(payload json, secret text, algorithm text default 'HS256')`](https://github.com/michelp/pgjwt#usage): Signs a JWT containing _payload_ with _secret_ using _algorithm_. +- [`verify(token text, secret text, algorithm text default 'HS256')`](https://github.com/michelp/pgjwt#usage): Decodes a JWT _token_ that was signed with _secret_ using _algorithm_. Where: diff --git a/apps/docs/content/guides/database/extensions/pgtap.mdx b/apps/docs/content/guides/database/extensions/pgtap.mdx index a4e678a06f0..e7a0f7843be 100644 --- a/apps/docs/content/guides/database/extensions/pgtap.mdx +++ b/apps/docs/content/guides/database/extensions/pgtap.mdx @@ -8,7 +8,7 @@ description: 'Unit testing in Postgres.' ## Overview -Let's cover some basic concepts: +This section covers basic concepts: - Unit tests: allow you to test small parts of a system (like a database table!). - TAP: stands for [Test Anything Protocol](http://testanything.org/). It is an framework which aims to simplify the error reporting during testing. diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 599f65be591..1e45230f2cf 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -9,7 +9,7 @@ tocVideo: 'agFsGDJxjwA' ## Overview -While you may be able to store simple lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability. +While you may be able to store lat/long geographic coordinates as a set of decimals, it does not scale very well when you try to query through a large data set. PostGIS comes with special data types that are efficient, and indexable for high scalability. The additional data types that PostGIS provides include [Point](https://postgis.net/docs/using_postgis_dbmanagement.html#Point), [Polygon](https://postgis.net/docs/using_postgis_dbmanagement.html#Polygon), [LineString](https://postgis.net/docs/using_postgis_dbmanagement.html#LineString), and many more to represent different types of geographical data. In this guide, we will mainly focus on how to interact with `Point` type, which represents a single set of latitude and longitude. If you are interested in digging deeper, you can learn more about different data types on the [data management section of PostGIS docs](https://postgis.net/docs/using_postgis_dbmanagement.html). @@ -47,9 +47,9 @@ drop extension if exists postgis; ## Examples -Now that we are ready to get started with PostGIS, let’s create a table and see how we can utilize PostGIS for some typical use cases. Let’s imagine we are creating a simple restaurant-searching app. +To get started with PostGIS, create a table and see to use PostGIS for some typical use cases. Imagine creating a basic restaurant-searching app. -Let’s create our table. Each row represents a restaurant with its location stored in `location` column as a `Point` type. +Create the table. Each row represents a restaurant with its location stored in `location` column as a `Point` type. ```sql create table if not exists public.restaurants ( @@ -205,7 +205,7 @@ We will create [database functions](/docs/guides/database/functions) so that we ### Order by distance -Sorting datasets from closest to farthest, sometimes called nearest-neighbor sort, is a very common use case in Geo-queries. PostGIS can handle it with the use of the [`<->`](https://postgis.net/docs/geometry_distance_knn.html) operator. `<->` operator returns the two-dimensional distance between two geometries and will utilize the spatial index when used within `order by` clause. You can create the following database function to sort the restaurants from closest to farthest by passing the current locations as parameters. +Sorting datasets from closest to farthest, sometimes called nearest-neighbor sort, is a very common use case in Geo-queries. PostGIS can handle it with the use of the [`<->`](https://postgis.net/docs/geometry_distance_knn.html) operator. `<->` operator returns the two-dimensional distance between two geometries and uses the spatial index when used within `order by` clause. You can create the following database function to sort the restaurants from closest to farthest by passing the current locations as parameters. ```sql create or replace function nearby_restaurants(lat float, long float) @@ -330,7 +330,7 @@ val data = supabase.postgrest.rpc( ![Searching within a bounding box of a map](/docs/img/guides/database/extensions/postgis/map.png) -When you are working on a map-based application where the user scrolls through your map, you might want to load the data that lies within the bounding box of the map every time your users scroll. PostGIS can return the rows that are within the bounding box just by supplying the bottom left and the top right coordinates. Let’s look at what the function would look like: +When you are working on a map-based application where the user scrolls through your map, you might want to load the data that lies within the bounding box of the map every time your users scroll. PostGIS can return the rows that are within the bounding box by supplying the bottom left and the top right coordinates. The function looks like this: ```sql create or replace function restaurants_in_view(min_lat float, min_long float, max_lat float, max_long float) @@ -344,7 +344,7 @@ as $$ $$; ``` -The [`&&`](https://postgis.net/docs/geometry_overlaps.html) operator used in the `where` statement here returns a boolean of whether the bounding box of the two geometries intersect or not. We are basically creating a bounding box from the two points and finding those points that fall under the bounding box. We are also utilizing a few different PostGIS functions: +The [`&&`](https://postgis.net/docs/geometry_overlaps.html) operator used in the `where` statement here returns a boolean of whether the bounding box of the two geometries intersect or not. It creates a bounding box from the two points and finds those points that fall under the bounding box. It also uses a few PostGIS functions: - [ST_MakeBox2D](https://postgis.net/docs/ST_MakeBox2D.html): Creates a 2-dimensional box from two points. - [ST_SetSRID](https://postgis.net/docs/ST_SetSRID.html): Sets the [SRID](https://postgis.net/docs/manual-dev/using_postgis_dbmanagement.html#spatial_ref_sys), which is an identifier of what coordinate system to use for the geometry. 4326 is the standard longitude and latitude coordinate system. diff --git a/apps/docs/content/guides/database/extensions/timescaledb.mdx b/apps/docs/content/guides/database/extensions/timescaledb.mdx index a3aa0303646..588927defb0 100644 --- a/apps/docs/content/guides/database/extensions/timescaledb.mdx +++ b/apps/docs/content/guides/database/extensions/timescaledb.mdx @@ -60,7 +60,7 @@ It's good practice to create the extension within a separate schema (like `exten ## Usage -To demonstrate how `timescaledb` works, let's consider a simple example where we have a table that stores temperature data from different sensors. We will create a table named "temperatures" and store data for two sensors. +To demonstrate how `timescaledb` works, consider an example where we have a table that stores temperature data from different sensors. Create a table named "temperatures" and store data for two sensors. First we create a hypertable, which is a virtual table that is partitioned into chunks based on time intervals. The hypertable acts as a proxy for the actual table and makes it easy to query and manage time-series data. diff --git a/apps/docs/content/guides/database/full-text-search.mdx b/apps/docs/content/guides/database/full-text-search.mdx index 89358757126..5104d8bb33c 100644 --- a/apps/docs/content/guides/database/full-text-search.mdx +++ b/apps/docs/content/guides/database/full-text-search.mdx @@ -831,7 +831,7 @@ don't need to be created at the time we execute the query. This will make our qu ### Searchable columns -Let's create a new column `fts` inside the `books` table to store the searchable index of the `title` and `description` columns. +Create a new column `fts` inside the `books` table to store the searchable index of the `title` and `description` columns. We can use a special feature of Postgres called [Generated Columns](https://www.postgresql.org/docs/current/ddl-generated-columns.html) diff --git a/apps/docs/content/guides/database/functions.mdx b/apps/docs/content/guides/database/functions.mdx index abea38ae77d..3c9c3511177 100644 --- a/apps/docs/content/guides/database/functions.mdx +++ b/apps/docs/content/guides/database/functions.mdx @@ -30,9 +30,9 @@ and run the SQL queries yourself. 3. Enter the SQL to create or replace your Database function. 4. Click "Run" or cmd+enter (ctrl+enter). -## Simple functions +## Basic functions [#simple-functions] -Let's create a basic Database Function which returns a string "hello world". +Create a basic database function that returns the string "hello world". ```sql create or replace function hello_world() -- 1 @@ -53,7 +53,7 @@ At it's most basic a function has the following parts: 2. `returns text`: The type of data that the function returns. If it returns nothing, you can `returns void`. 3. `language sql`: The language used inside the function body. This can also be a procedural language: `plpgsql`, `plpython`, etc. 4. `as $$`: The function wrapper. Anything enclosed inside the `$$` symbols will be part of the function body. -5. `select 'hello world';`: A simple function body. The final `select` statement inside a function body will be returned if there are no statements following it. +5. `select 'hello world';`: A basic function body. The final `select` statement inside a function body will be returned if there are no statements following it. 6. `$$;`: The closing symbols of the function wrapper. @@ -290,7 +290,7 @@ data = supabase.rpc('get_planets').eq('id', 1).execute() ## Passing parameters -Let's create a Function to insert a new planet into the `planets` table and return the new ID. Note that this time we're using the `plpgsql` language. +Create a function to insert a new planet into the `planets` table and return the new ID. Note that this time we're using the `plpgsql` language. ```sql create or replace function add_planet(name text) diff --git a/apps/docs/content/guides/database/inspect.mdx b/apps/docs/content/guides/database/inspect.mdx index 0f0fc5a3945..965e83912a2 100644 --- a/apps/docs/content/guides/database/inspect.mdx +++ b/apps/docs/content/guides/database/inspect.mdx @@ -10,7 +10,7 @@ Database performance is a large topic and many factors can contribute. Some of t - A lack of indexes causing slower than required queries over large tables - Unused indexes causing slow `INSERT`, `UPDATE` and `DELETE` operations - Not enough compute resources, such as memory, causing your database to go to disk for results too often -- Lock contention from multiple queries operating on highly utilized tables +- Lock contention from multiple queries operating on heavily used tables - Large amount of bloat on your tables causing poor query planning You can examine your database and queries for these issues using either the [Supabase CLI](/docs/guides/local-development/cli/getting-started) or SQL. @@ -262,4 +262,4 @@ Using the query plan analyzer to optimize your queries is a large topic, with a - [Postgres Wiki.](https://wiki.postgresql.org/wiki/Using_EXPLAIN) - [Enterprise DB.](https://www.enterprisedb.com/blog/postgresql-query-optimization-performance-tuning-with-explain-analyze) -You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../platform/metrics) to better understand the behavior of your DB and the queries you're executing against it. +You can pair the information available from `pg_stat_statements` with the detailed system metrics available [via your metrics endpoint](../telemetry/metrics) to better understand the behavior of your DB and the queries you're executing against it. diff --git a/apps/docs/content/guides/database/joins-and-nesting.mdx b/apps/docs/content/guides/database/joins-and-nesting.mdx index 361180a3a6c..f9b372ab745 100644 --- a/apps/docs/content/guides/database/joins-and-nesting.mdx +++ b/apps/docs/content/guides/database/joins-and-nesting.mdx @@ -8,7 +8,7 @@ The data APIs automatically detect relationships between Postgres tables. Since ## One-to-many joins -Let's use an example database that stores `orchestral_sections` and `instruments`: +Use an example database that stores `orchestral_sections` and `instruments`: -### Table view +You can work with a project's database in several ways: -You don't have to be a database expert to start using Supabase. Our table view makes Postgres as easy to use as a spreadsheet. +- Visually using the [**Table Editor**](/dashboard/project/_/editor) section of the Dashboard. +- With query syntax using the [**SQL Editor**](/dashboard/project/_/sql) section of the Dashboard. +- Programmatically using a variety of methods. -![Table View.](/docs/img/table-view.png) - -### Relationships - -Dig into the relationships within your data. - - - -### Clone tables - -You can duplicate your tables, just like you would inside a spreadsheet. - - - -### The SQL editor - -Supabase comes with a SQL Editor. You can also save your favorite queries to run later! - - - -### Additional features - -- Supabase extends Postgres with realtime functionality using our [Realtime Server](https://github.com/supabase/realtime). -- Every project is a full Postgres database, with `postgres` level access. -- Supabase manages your database backups. -- Import data directly from a CSV or excel spreadsheet. - - - -Database backups **do not** include objects stored via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects that have been deleted since then. +Read the [Connect to your database](/docs/guides/database/connecting-to-postgres#how-to-connect-to-your-postgres-databases) guide for details on connection strings and options. -### Extensions +The 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. -To expand the functionality of your Postgres database, you can use extensions. -You can enable Postgres extensions with the click of a button within the Supabase dashboard. +## Get started - +If you're new to the database section, these are the pages to read first: -[Learn more](/docs/guides/database/extensions) about all the extensions provided on Supabase. +- **[Connect to your database](/docs/guides/database/connecting-to-postgres)**: Connection strings, the Supavisor connection pooler, and when to use direct, transaction, or session mode. +- **[Tables and data](/docs/guides/database/tables)**: Create tables and relationships, and edit rows from the Dashboard. +- **[Import data](/docs/guides/database/import-data)**: Load existing data from CSV files, `pg_dump`, or another Postgres database. +- **[Secure your data](/docs/guides/database/secure-data)**: Row Level Security (RLS) is how Supabase makes the database safe to query directly from the client. Read this before exposing any table to your app. +- **[Extensions](/docs/guides/database/extensions)**: Enable Postgres extensions — including `pgvector` for embeddings, `PostGIS` for geospatial data, and `pg_cron` for scheduled jobs — from the Dashboard. +- **[Run SQL commands](/dashboard/project/_/sql)**: Use the Dashboard's SQL Editor for ad-hoc queries and saved snippets. -## Terminology +## Going further -{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} +Once you've covered the basics, these guides help with other use cases and features: -### Postgres or PostgreSQL? - -{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} - -PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986. This package included a query language called "PostQUEL". - -In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL. -{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} - -Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability. -After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about simplicity, so we also refer to it as Postgres. - -## Tips - -Read about resetting your database password [here](/docs/guides/database/managing-passwords) and changing the timezone of your server [here](/docs/guides/database/managing-timezones). - -## Next steps - -- Read more about [Postgres](https://www.postgresql.org/about/) -- Sign in: [supabase.com/dashboard](/dashboard) +- **[Database functions](/docs/guides/database/functions)** and **[triggers](/docs/guides/database/postgres/triggers)**: Run logic inside the database in response to inserts, updates, or deletes. +- **[Database webhooks](/docs/guides/database/webhooks)**: Send row changes to an external HTTP endpoint. +- **[Replication and read replicas](/docs/guides/database/replication)**: Stream changes to other systems or read from a geographically closer replica. +- **[Backups](/docs/guides/platform/backups)**: Daily backups on every project, with point-in-time recovery on paid plans. Backups cover the database itself; objects stored through the Storage API are not included. +- **[Query performance and optimization](/docs/guides/database/query-optimization)**: Indexes, the query planner, and tools for finding slow queries. +- **[Roles and permissions](/docs/guides/database/postgres/roles)**: The Postgres roles Supabase ships with and how to add your own. diff --git a/apps/docs/content/guides/database/partitions.mdx b/apps/docs/content/guides/database/partitions.mdx index 9019ea55e34..8b18ef2c070 100644 --- a/apps/docs/content/guides/database/partitions.mdx +++ b/apps/docs/content/guides/database/partitions.mdx @@ -36,7 +36,7 @@ Postgres supports various partitioning methods based on how you want to partitio ## Creating partitioned tables -Let's consider an example of range partitioning for a sales table based on the order date. We'll create monthly partitions to store data for each month: +The following example uses range partitioning for a sales table based on the order date. We'll create monthly partitions to store data for each month: ```sql create table sales ( @@ -105,11 +105,11 @@ There is no real threshold to determine when you should use partitions. Partitio ## Examples -Here are simple examples for each of the partitioning types in Postgres. +Here are basic examples for each of the partitioning types in Postgres. ### Range partitioning -Let's consider a range partitioning example for a table that stores sales data based on the order date. We'll create monthly partitions to store data for each month. +The following range partitioning example stores sales data based on the order date. We'll create monthly partitions to store data for each month. In this example, the **`sales`** table is partitioned into two partitions: **`sales_january`** and **`sales_february`**. The data in these partitions is based on the specified range of order dates: @@ -137,7 +137,7 @@ create table sales_2000_02 ### List partitioning -Let's consider a list partitioning example for a table that stores customer data based on their region. We'll create partitions to store customers from different regions. +The following list partitioning example stores customer data based on their region. We'll create partitions to store customers from different regions. In this example, the **`customers`** table is partitioned into two partitions: `customers_americas` and `customers_asia`. The data in these partitions is based on the specified list of regions: diff --git a/apps/docs/content/guides/database/postgres/cascade-deletes.mdx b/apps/docs/content/guides/database/postgres/cascade-deletes.mdx index a250cb473fc..e3a73311ce0 100644 --- a/apps/docs/content/guides/database/postgres/cascade-deletes.mdx +++ b/apps/docs/content/guides/database/postgres/cascade-deletes.mdx @@ -38,7 +38,7 @@ In practice, you can use either `NO ACTION` or `RESTRICT` depending on your need ## Example -Let's further illustrate the difference with an example. We'll use the following data: +To illustrate the difference, use the following example. We'll use the following data: `grandparent` @@ -117,7 +117,7 @@ Even though the foreign key constraint between parent and grandparent is `CASCAD ### `NO ACTION` -Let's change the child-father relationship to `NO ACTION`: +Change the child-father relationship to `NO ACTION`: ```sql alter table child @@ -157,7 +157,7 @@ ERROR: update or delete on table "parent" violates foreign key constraint "child DETAIL: Key (id)=(1) is still referenced from table "child". ``` -But, when we combine it with _other_ constraints, then any other constraints take precedence. For example, let's run the same but add a `mother` column that has a `CASCADE` delete: +But, when we combine it with _other_ constraints, then any other constraints take precedence. For example, run the same scenario but add a `mother` column that has a `CASCADE` delete: ```sql alter table child @@ -169,7 +169,7 @@ set mother = 2 where id = 1; ``` -Then let's run a delete on the `grandparent` table: +Then run a delete on the `grandparent` table: ```shell postgres=# delete from grandparent; diff --git a/apps/docs/content/guides/database/postgres/column-level-security.mdx b/apps/docs/content/guides/database/postgres/column-level-security.mdx index 4c4e6ffb12f..13937a5fc64 100644 --- a/apps/docs/content/guides/database/postgres/column-level-security.mdx +++ b/apps/docs/content/guides/database/postgres/column-level-security.mdx @@ -22,7 +22,7 @@ Restricted roles cannot use the wildcard operator (`*`) on the affected table. I Policies in Row Level Security (RLS) are used to restrict access to rows in a table. Think of them like adding a `WHERE` clause to every query. -For example, let's assume you have a `posts` table with the following columns: +For example, assume you have a `posts` table with the following columns: - `id` - `user_id` @@ -134,7 +134,7 @@ supabase migration new create_posts_table user_id text, title text, content text, - created_at timestamptz default now() + created_at timestamptz default now(), updated_at timestamptz default now() ); diff --git a/apps/docs/content/guides/database/postgres/data-deletion.mdx b/apps/docs/content/guides/database/postgres/data-deletion.mdx index 685dcc44f80..a27ac37074c 100644 --- a/apps/docs/content/guides/database/postgres/data-deletion.mdx +++ b/apps/docs/content/guides/database/postgres/data-deletion.mdx @@ -66,7 +66,7 @@ WHERE id IN ( This approach has the benefit of controlling when it runs, locking for a shorter period of time and minimising impact on other transactions. -If you know in advance that such large deletes will have to happen in the business cycle of your database, then you should seriously think about using (table parititioning)[/docs/guides/database/partitions] as a management tool. +If you know in advance that such large deletes will have to happen in the business cycle of your database, then you should seriously think about using [table partitioning](/docs/guides/database/partitions) as a management tool. ### Soft deletes @@ -202,4 +202,4 @@ The most efficient way to reclaim disk space, without locks, is to use [pg_repac - [Safe Cascading Deletes](/docs/guides/database/postgres/cascade-deletes) - [Inspecting your Database](/docs/guides/database/inspect) - [Understanding Database and Disk Size](/docs/guides/platform/database-size) -- [Bloat in Postgres](/docs/blog/postgres-bloat) +- [Bloat in Postgres](/blog/postgres-bloat) diff --git a/apps/docs/content/guides/database/postgres/enums.mdx b/apps/docs/content/guides/database/postgres/enums.mdx index 6fbb8d4b602..8d8ff726b79 100644 --- a/apps/docs/content/guides/database/postgres/enums.mdx +++ b/apps/docs/content/guides/database/postgres/enums.mdx @@ -93,7 +93,7 @@ where name = 'Alice'; To add new values to an existing Postgres Enum, you can use the `ALTER TYPE` statement. Here's how you can do it: -Let's say you have an existing Enum called `mood`, and you want to add a new value, `content`: +Say you have an existing enum called `mood`, and you want to add a new value, `content`: {/* prettier-ignore */} ```sql diff --git a/apps/docs/content/guides/database/postgres/first-row-in-group.mdx b/apps/docs/content/guides/database/postgres/first-row-in-group.mdx index f94891a5c4a..b20a3eaa8b8 100644 --- a/apps/docs/content/guides/database/postgres/first-row-in-group.mdx +++ b/apps/docs/content/guides/database/postgres/first-row-in-group.mdx @@ -32,10 +32,9 @@ select distinct points from seasons -order BY - id, - points desc, - team; +order by + team, + points desc; ``` The important bits here are: diff --git a/apps/docs/content/guides/database/postgres/indexes.mdx b/apps/docs/content/guides/database/postgres/indexes.mdx index d0b36b77bf0..3263e5a6e6a 100644 --- a/apps/docs/content/guides/database/postgres/indexes.mdx +++ b/apps/docs/content/guides/database/postgres/indexes.mdx @@ -13,7 +13,7 @@ Even though indexes improve query performance, the Postgres query planner may no ## Create an index -Let's take an example table: +Start with an example table: ```sql create table persons ( @@ -53,7 +53,7 @@ Seq Scan on persons (cost=0.00..22.75 rows=x width=y) Filter: (age = 32) ``` -To add a simple B-Tree index you can run: +To add a basic B-Tree index you can run: ```sql create index idx_persons_age on persons (age); diff --git a/apps/docs/content/guides/database/postgres/roles.mdx b/apps/docs/content/guides/database/postgres/roles.mdx index e5271f84df9..3286c786e49 100644 --- a/apps/docs/content/guides/database/postgres/roles.mdx +++ b/apps/docs/content/guides/database/postgres/roles.mdx @@ -113,7 +113,15 @@ Used by the Auth middleware to connect to the database and run migration. Access ### `supabase_etl_admin` -Used by [Replication powered by Supabase ETL](/docs/guides/database/replication) to replicate database changes to external destinations. Has read-all access and replication privileges for change data capture, bypasses Row Level Security, and can write to the `etl` schema. +`supabase_etl_admin` is used by [Database replication](/docs/guides/database/replication) through Supabase ETL. + +This role: + +- Replicates database changes to destination systems +- Has read-all access +- Has replication privileges for change data capture, bypasses Row Level Security +- Can create event triggers +- Can write to the `etl` schema ### `dashboard_user` diff --git a/apps/docs/content/guides/database/postgres/row-level-security.mdx b/apps/docs/content/guides/database/postgres/row-level-security.mdx index ca895abf839..3e1112bc60e 100644 --- a/apps/docs/content/guides/database/postgres/row-level-security.mdx +++ b/apps/docs/content/guides/database/postgres/row-level-security.mdx @@ -172,7 +172,7 @@ Supabase provides some [helpers](#helper-functions) that simplify RLS if you're You can specify select policies with the `using` clause. -Let's say you have a table called `profiles` in the public schema and you want to enable read access to everyone. +Say you have a table called `profiles` in the public schema and you want to enable read access to everyone. ```sql -- 1. Create table @@ -204,7 +204,7 @@ for select using ( (select auth.uid()) = user_id ); You can specify insert policies with the `with check` clause. The `with check` expression ensures that any new row data adheres to the policy constraints. -Let's say you have a table called `profiles` in the public schema and you only want users to be able to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert: +Say you have a table called `profiles` in the public schema and you only want users to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert: ```sql -- 1. Create table @@ -230,7 +230,7 @@ You can specify update policies by combining both the `using` and `with check` e The `using` clause represents the condition that must be true for the update to be allowed, and `with check` clause ensures that the updates made adhere to the policy constraints. -Let's say you have a table called `profiles` in the public schema and you only want users to be able to update their own profile. +Say you have a table called `profiles` in the public schema and you only want users to update their own profile. You can create a policy where the `using` clause checks if the user owns the profile being updated. And the `with check` clause ensures that, in the resultant row, users do not change the `user_id` to a value that is not equal to their User ID, maintaining that the modified profile still meets the ownership condition. @@ -265,7 +265,7 @@ To perform an `UPDATE` operation, a corresponding [`SELECT` policy](#select-poli You can specify delete policies with the `using` clause. -Let's say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile: +Say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile: ```sql -- 1. Create table diff --git a/apps/docs/content/guides/database/prisma.mdx b/apps/docs/content/guides/database/prisma.mdx index 31ea39f16e5..ad89d25702d 100644 --- a/apps/docs/content/guides/database/prisma.mdx +++ b/apps/docs/content/guides/database/prisma.mdx @@ -78,7 +78,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npm init -y - npm install prisma typescript ts-node @types/node --save-dev + npm install prisma tsx @types/pg --save-dev + npm install @prisma/client @prisma/adapter-pg dotenv pg npx tsc --init @@ -87,8 +88,9 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash - pnpm init -y - pnpm install prisma typescript ts-node @types/node --save-dev + pnpm init + pnpm install prisma tsx @types/pg --save-dev + pnpm install @prisma/client @prisma/adapter-pg dotenv pg pnpx tsc --init @@ -98,7 +100,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash yarn init -y - yarn add prisma typescript ts-node @types/node --save-dev + yarn add prisma tsx @types/pg --save-dev + yarn add @prisma/client @prisma/adapter-pg dotenv pg npx tsc --init @@ -108,7 +111,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash bun init -y - bun install prisma typescript ts-node @types/node --save-dev + bun install prisma tsx @types/pg --save-dev + bun install @prisma/client @prisma/adapter-pg dotenv pg bunx tsc --init @@ -168,15 +172,6 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t postgres://prisma.[PROJECT-REF]... ``` - In your schema.prisma file, edit your `datasource db` configs to reference your DIRECT_URL - ```text schema.prisma - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") - } - ``` - @@ -190,9 +185,56 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t - + - If you have already modified your Supabase database, synchronize it with your migration file. Otherwise create new tables for your database + Add `import "dotenv/config"` to the generated `prisma.config.ts`. If you are using a serverless environment, change the data source URL to `DIRECT_URL`. + + + + + + + ```ts prisma.config.ts + import "dotenv/config"; + import { defineConfig, env } from "prisma/config"; + + export default defineConfig({ + schema: "prisma/schema", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, + }); + ``` + + + ```ts prisma.config.ts + import "dotenv/config"; + import { defineConfig, env } from "prisma/config"; + + export default defineConfig({ + schema: "prisma/schema", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DIRECT_URL"), + }, + }); + ``` + + + + + + + + + + + If you have already modified your Supabase database, synchronize it with your migration file. Otherwise create new tables for your database, then generate the Prisma client. @@ -232,25 +274,25 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npx prisma migrate dev --name first_prisma_migration - + npx prisma generate ``` ```bash pnpx prisma migrate dev --name first_prisma_migration - + pnpx prisma generate ``` ```bash npx prisma migrate dev --name first_prisma_migration - + npx prisma generate ``` ```bash bunx prisma migrate dev --name first_prisma_migration - + bunx prisma generate ``` @@ -291,6 +333,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npx prisma migrate resolve --applied 0_init_supabase + npx prisma generate ```
@@ -319,6 +362,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash pnpx prisma migrate resolve --applied 0_init_supabase + pnpx prisma generate ``` @@ -347,6 +391,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npx prisma migrate resolve --applied 0_init_supabase + npx prisma generate ``` @@ -369,12 +414,13 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ``` - If there are any conflicts, reference [Prisma's official doc](https://www.prisma.io/docs/orm/prisma-migrate/getting-started#work-around-features-not-supported-by-prisma-schema-language) or the [trouble shooting guide](/docs/guides/database/prisma-troubleshooting) for more details + If there are any conflicts, reference [Prisma's official doc](https://www.prisma.io/docs/orm/prisma-migrate/getting-started#work-around-features-not-supported-by-prisma-schema-language) or the [trouble shooting guide](/docs/guides/database/prisma/prisma-troubleshooting) for more details ```bash bunx prisma migrate resolve --applied 0_init_supabase + bunx prisma generate ``` @@ -385,47 +431,6 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t - - - - Install the Prisma client and generate its model - - - - - - ```sh - npm install @prisma/client - npx prisma generate - ``` - - - ```sh - pnpm install @prisma/client - pnpx prisma generate - ``` - - - ```sh - yarn add @prisma/client - npx prisma generate - ``` - - - ```sh - bun install @prisma/client - bunx prisma generate - ``` - - - - @@ -433,13 +438,15 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```ts index.ts - const { PrismaClient } = require('@prisma/client'); + import "dotenv/config"; + import { PrismaClient } from "./generated/prisma/client"; + import { PrismaPg } from "@prisma/adapter-pg"; - const prisma = new PrismaClient(); + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); + export const prisma = new PrismaClient({ adapter }); async function main() { - //change to reference a table in your schema - const val = await prisma..findMany({ + const val = await prisma.user.findMany({ take: 10, }); console.log(val); @@ -452,10 +459,10 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t .catch(async (e) => { console.error(e); await prisma.$disconnect(); - process.exit(1); + process.exit(1); }); - ``` + diff --git a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx index fb0f3bd2511..4918a182eb2 100644 --- a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx +++ b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx @@ -25,15 +25,15 @@ These options, called "query parameters," can be used to address specific errors connection_string.../postgres?KEY1=VALUE&KEY2=VALUE&KEY3=VALUE ``` -# Errors +## Errors {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} -## ... prepared statement already exists +### Prepared statement already exists Supavisor in transaction mode (port 6543) does not support [prepared statements](https://www.postgresql.org/docs/current/sql-prepare.html), which Prisma will try to create in the background. -### Solution: [#solution-prepared-statement-exists] +#### Solution: [#solution-prepared-statement-exists] - Add `pgbouncer=true` to the connection string. This turns off prepared statements in Prisma. @@ -43,17 +43,17 @@ Supavisor in transaction mode (port 6543) does not support [prepared statements] --- -## Can't reach database server at: +### Can't reach the database server -Prisma couldn't establish a connection with Postgres or Supavisor before the timeout +Prisma couldn't establish a connection with Postgres or Supavisor before the timeout. -### Possible causes: [#possible-causes-cant-reach-database-server-at] +#### Possible causes: [#possible-causes-cant-reach-database-server-at] - **Database overload**: The database server is under heavy load, causing Prisma to struggle to connect. - **Malformed connection string**: The connection string used by Prisma is incorrect or incomplete. - **Transient network issues**: Temporary network problems are disrupting the connection. -### Solutions: [#solution-cant-reach-database-server-at] +#### Solutions: [#solution-cant-reach-database-server-at] - **Check database health**: Use the [Observability Dashboard](/dashboard/project/_/observability/database) to monitor CPU, memory, and I/O usage. If the database is overloaded, consider increasing your [compute size](/docs/guides/platform/compute-add-ons) or [optimizing your queries](/docs/guides/database/query-optimization). - **Verify connection string**: Double-check the connection string in your Prisma configuration to ensure it matches in your [project connect page](/dashboard/project/_?showConnect=true). @@ -65,17 +65,17 @@ Prisma couldn't establish a connection with Postgres or Supavisor before the tim --- -## Timed out fetching a new connection from the connection pool: +### Timed out fetching a new connection from the connection pool Prisma is unable to allocate connections to pending queries fast enough to meet demand. -### Possible causes: [#possible-causes-timed-out-fetching-a-new-connection] +#### Possible causes: [#possible-causes-timed-out-fetching-a-new-connection] - **Overwhelmed server**: The server hosting Prisma is under heavy load, limiting its ability to manage connections. By default, Prisma will create the default `num_cpus * 2 + 1` worth of connections. A common cause for server strain is increasing the `connection_limit` significantly past the default. - **Insufficient pool size**: The Supavisor pooler does not have enough connections available to quickly satisfy Prisma's requests. - **Slow queries**: Prisma's queries are taking too long to execute, preventing it from releasing connections for reuse. -### Solutions: [#solution-timed-out-fetching-a-new-connection] +#### Solutions: [#solution-timed-out-fetching-a-new-connection] - **Increase the pool timeout**: Increase the `pool_timeout` parameter in your Prisma configuration to give the pooler more time to allocate connections. - **Reduce the connection limit**: If you've explicitly increased the `connection_limit` parameter in your Prisma configuration, try reducing it to a more reasonable value. @@ -85,43 +85,43 @@ Prisma is unable to allocate connections to pending queries fast enough to meet --- -## Server has closed the connection +### Server has closed the connection According to this [GitHub Issue for Prisma](https://github.com/prisma/prisma/discussions/7389), this error may be related to large return values for queries. It may also be caused by significant database strain. -### Solutions: [#solution-server-has-closed-the-connection] +#### Solutions: [#solution-server-has-closed-the-connection] - **Limit row return sizes**: Try to limit the total amount of rows returned for particularly large requests. - **Minimize database strain**:Check the Reports Page for database strain. If there is obvious strain, consider [optimizing](/docs/guides/database/query-optimization) or increasing compute size --- -## Drift detected: Your database schema is not in sync with your migration history +### Drift detected: Your database schema is not in sync with your migration history Prisma relies on migration files to ensure your database aligns with Prisma's model. External schema changes are detected as "drift", which Prisma will try to overwrite, potentially causing data loss. -### Possible causes: [#possible-causes-your-database-schema-is-not-in-sync] +#### Possible causes: [#possible-causes-your-database-schema-is-not-in-sync] - **Supabase Managed Schemas**: Supabase may update managed schemas like auth and storage to introduce new features. Granting Prisma access to these schemas can lead to drift during updates. - **External Schema Modifications**: Your team or another tool might have modified the database schema outside of Prisma, causing drift. -### Solution: [#solution-your-database-schema-is-not-in-sync] +#### Solution: [#solution-your-database-schema-is-not-in-sync] - **Baselining migrations**: [baselining](https://www.prisma.io/docs/orm/prisma-migrate/workflows/baselining) re-syncs Prisma by capturing the current database schema as the starting point for future migrations. --- -## Max client connections reached +### Max client connections reached Postgres or Supavisor rejected a request for more connections -### Possible causes:[#possible-causes-max-client-connections-reached] +#### Possible causes:[#possible-causes-max-client-connections-reached] - **When working in transaction mode (port 6543):** The error "Max client connections reached" occurs when clients try to form more connections with the pooler than it can support. - **When working in session mode (port 5432):** The max amount of clients is restricted to the "Pool Size" value in the [Database Settings](/dashboard/project/_/database/settings). If the "Pool Size" is set to 15, even if the pooler can handle 200 client connections, it will still be effectively capped at 15 for each unique ["database-role+database" combination](https://github.com/orgs/supabase/discussions/21566). - **When working with direct connections**: Postgres is already servicing the max amount of connections -### Solutions [#solutions-causes-max-client-connections-reached] +#### Solutions [#solutions-causes-max-client-connections-reached] - **Transaction Mode for serverless apps**: If you are using serverless functions (Supabase Edge, Vercel, AWS Lambda), switch to transaction mode (port 6543). It handles more connections than session mode or direct connections. - **Reduce the number of Prisma connections**: A single client-server can establish multiple connections with a pooler. Typically, serverless setups do not need many connections. Starting with fewer, like five or three, or even just one, is often sufficient. In serverless setups, begin with `connection_limit=1`, increasing cautiously if needed to avoid maxing out connections. @@ -132,28 +132,26 @@ Postgres or Supavisor rejected a request for more connections --- -## Cross schema references are only allowed when the target schema is listed in the schemas property of your data-source +### Cross schema references are only allowed when the target schema is listed in the schemas property of your data-source A Prisma migration is referencing a schema it is not permitted to manage. -### Possible causes: [#possible-causes-cross-schema-references] +#### Possible causes: [#possible-causes-cross-schema-references] - A migration references a schema that Prisma is not permitted to manage -### Solutions: [#solutions-cross-schema-references] +#### Solutions: [#solutions-cross-schema-references] -- Multi-Schema support: If the external schema isn't Supabase managed, modify your `prisma.schema` file to enable the multi-Schema preview +- Multi-schema support: If the external schema isn't Supabase managed, list the relevant schemas on the `datasource` block in your `schema.prisma` file. -```ts prisma.schema +```prisma schema.prisma generator client { - provider = "prisma-client-js" - previewFeatures = ["multiSchema"] //Add line + provider = "prisma-client" + output = "../generated/prisma" } datasource db { provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") schemas = ["public", "other_schema"] //list out relevant schemas } ``` diff --git a/apps/docs/content/guides/database/replication.mdx b/apps/docs/content/guides/database/replication.mdx index 53a92e048be..8ff0b26278f 100644 --- a/apps/docs/content/guides/database/replication.mdx +++ b/apps/docs/content/guides/database/replication.mdx @@ -1,7 +1,7 @@ --- id: 'replication' title: 'Database replication' -description: 'Compare read replicas, external replication, and manual replication.' +description: 'Compare read replicas, external replication (ETL), and manual replication.' subtitle: 'An introduction to database replication and change data capture.' sidebar_label: 'Overview' --- @@ -18,7 +18,7 @@ You might use database replication for: ## Replication methods -Supabase supports three replication methods. Choose based on whether you need another Supabase Postgres database, a managed pipeline to an external system, or full control over your own logical replication setup. +Supabase supports three replication methods. Choose based on whether you need another Supabase Postgres database, a managed pipeline to a destination system, or full control over your own logical replication setup. ### Read replicas @@ -26,21 +26,21 @@ Read replicas are additional Supabase Postgres databases kept in sync with your - [Set up read replicas](/docs/guides/platform/read-replicas) -### External replication +### External replication (ETL) [#external-replication] -External replication is currently in private alpha. Access is limited and features may change. +External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. -External replication is powered by [Supabase ETL](https://github.com/supabase/etl). It uses Postgres logical replication under the hood and provides a managed Dashboard workflow for replicating data from Supabase Postgres to external data systems. +External replication (ETL) is powered by [Supabase ETL](https://github.com/supabase/etl). It uses Postgres logical replication under the hood and provides a managed Dashboard workflow for replicating data from Supabase Postgres to destination systems. In this context, "external" means outside the source database. Destinations can be Supabase-managed or third-party systems as support expands. -- [Set up external replication](/docs/guides/database/replication/external-replication-setup) +- [Set up external replication (ETL)](/docs/guides/database/replication/external-replication-setup) #### Supported destinations -External replication currently supports BigQuery as the managed destination. We are working on new destinations, and this table will be updated as support expands. +External replication (ETL) currently supports BigQuery as the managed destination. We are working on new destinations, and this table will be updated as support expands. | Destination | Insert | Update | Delete | Truncate | Schema change | Description | | ------------------------------------------------------ | ------------ | ------------ | ------------ | ------------ | ------------- | ------------------------------------------------------------------- | @@ -48,7 +48,7 @@ External replication currently supports BigQuery as the managed destination. We ### Manual replication -Manual replication uses the same underlying Postgres logical replication features as external replication, but you configure and operate the pieces yourself. Use this path when you want to connect tools such as Airbyte, Estuary, Fivetran, Materialize, Stitch, AWS DMS, or another system that supports Postgres logical replication. +Manual replication uses the same underlying Postgres logical replication features as external replication (ETL), but you configure and operate the pieces yourself. Use this path when you want to connect tools such as Airbyte, Estuary, Fivetran, Materialize, Stitch, AWS DMS, or another system that supports Postgres logical replication. - [Set up manual replication](/docs/guides/database/replication/manual-replication-setup) diff --git a/apps/docs/content/guides/database/replication/bigquery.mdx b/apps/docs/content/guides/database/replication/bigquery.mdx index c583f3a095e..12f1ae14fc9 100644 --- a/apps/docs/content/guides/database/replication/bigquery.mdx +++ b/apps/docs/content/guides/database/replication/bigquery.mdx @@ -1,14 +1,14 @@ --- id: 'bigquery-destination' title: 'BigQuery destination' -description: 'Configure BigQuery as an external replication destination.' +description: 'Configure BigQuery as an external replication (ETL) destination.' subtitle: 'Replicate Supabase Postgres tables to BigQuery.' sidebar_label: 'BigQuery' --- -External replication is currently in private alpha. Access is limited and features may change. +External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. @@ -29,17 +29,22 @@ Before configuring BigQuery as a destination, set up the following in Google Clo 3. **GCP service account key**: Create a [service account](https://cloud.google.com/iam/docs/keys-create-delete) with appropriate permissions - Go to **IAM & Admin > Service Accounts** - Click **Create Service Account** - - Grant the "BigQuery Data Editor" role + - Grant the "BigQuery Data Editor" and "BigQuery Job User" roles - Create and download the JSON key file Required permissions: - `bigquery.datasets.get` +- `bigquery.jobs.create` - `bigquery.tables.create` +- `bigquery.tables.delete` - `bigquery.tables.get` - `bigquery.tables.getData` +- `bigquery.tables.list` - `bigquery.tables.update` - `bigquery.tables.updateData` +- `bigquery.routines.get` +- `bigquery.routines.list` ## Configure BigQuery as a destination @@ -85,19 +90,25 @@ Once configured, replication to BigQuery: ## Source table requirements -BigQuery replication requires each source table to have a primary key, and the publication must include the primary-key columns. External replication declares those columns as the BigQuery destination primary key so BigQuery CDC can apply `UPSERT` and `DELETE` rows. +BigQuery replication requires each source table to have a primary key, and the publication must include the primary-key columns. Supabase ETL declares those columns as the BigQuery destination primary key so BigQuery change data capture (CDC) can apply `UPSERT` and `DELETE` rows. -BigQuery primary keys are `NOT ENFORCED`, and BigQuery CDC supports composite primary keys with up to 16 columns. Your source primary key must stay unique and non-null because BigQuery uses it to match CDC rows. +BigQuery primary keys are `NOT ENFORCED`, and BigQuery change data capture (CDC) supports composite primary keys with up to 16 columns. Your source primary key must stay unique and non-null because BigQuery uses it to match CDC rows. -Source tables must also use a BigQuery-compatible Postgres `REPLICA IDENTITY` setting: +Source tables must also use a BigQuery-compatible Postgres `REPLICA IDENTITY` setting. Most tables can keep the Postgres default, as long as they have a primary key and all primary-key columns are included in the publication. -- `DEFAULT` with a primary key is supported and works for most tables. -- `FULL` is supported and is recommended for tables with large `text`, `jsonb`, `bytea`, or other values that Postgres may store out-of-line using TOAST. -- `USING INDEX`, `NOTHING`, or `DEFAULT` without a primary key are not supported for BigQuery replication. +| Source table setting | BigQuery support | Guidance | +| ------------------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `REPLICA IDENTITY DEFAULT` with a primary key | Supported | Recommended for most tables. BigQuery uses the replicated source primary key to apply upserts and deletes. | +| `REPLICA IDENTITY FULL` | Supported | Recommended for tables with large `text`, `jsonb`, `bytea`, or other values that Postgres may store out-of-line using TOAST, especially when those rows update. | +| `REPLICA IDENTITY USING INDEX` | Not supported | BigQuery change data capture (CDC) rows are keyed by the source primary key, not by an alternative unique index. | +| `REPLICA IDENTITY NOTHING` | Not supported | Updates and deletes do not include enough row identity for BigQuery to apply them safely. | +| `REPLICA IDENTITY DEFAULT` without a primary key | Not supported | BigQuery requires a source primary key. | For a general explanation of how replica identity affects update and delete events, see [How does replica identity affect updates and deletes?](/docs/guides/database/replication/external-replication-faq#how-does-replica-identity-affect-updates-and-deletes). -For updates, Postgres does not always send a complete old row through logical replication. It can also mark unchanged toasted values as `unchanged toast` instead of resending the value. The replication pipeline can reconstruct a complete update when the old row image contains the missing value, which is reliable with `REPLICA IDENTITY FULL`. BigQuery CDC upserts require a complete new row, so updates can fail for tables with toasted columns if the pipeline receives only a partial update row. +For updates, Postgres does not always send a complete old row through logical replication. It can also mark unchanged toasted values as `unchanged toast` instead of resending the value. BigQuery change data capture (CDC) upserts require a complete new row because omitted columns are not preserved in the destination. The replication pipeline can reconstruct a complete update when the old row image contains the missing value, which is reliable with `REPLICA IDENTITY FULL`. + +If a BigQuery pipeline fails with an error about a partial update row, set `REPLICA IDENTITY FULL` on the affected source table and restart the pipeline. Changing replica identity only affects new WAL records, so a retained update that was written before the change may still need to be skipped by recreating the pipeline or re-copying the affected table. Check a table's current replica identity: @@ -134,20 +145,26 @@ This structure handles table truncations while maintaining query compatibility. ## Schema change support -Schema change support for BigQuery is currently in beta. External replication supports a limited set of schema changes while the feature is developed further. +Schema change support for BigQuery is currently in beta. Supabase ETL supports a limited set of schema changes for BigQuery while the feature is developed further. Supported schema changes: -- Adding a column +- Adding a nullable column - Removing a column - Renaming a column +- Dropping a `NOT NULL` constraint +- Setting or dropping supported column default metadata -Unsupported schema changes: +Unsupported or limited schema changes: - Changing a column's data type -- Replicating column default values +- Adding `NOT NULL` with `SET NOT NULL` +- Filling existing rows for `ADD COLUMN ... DEFAULT` +- Unsupported default expressions -We plan to expand schema change support over time as the feature evolves. +BigQuery requires added columns to be nullable. When a replicated `ADD COLUMN` includes a default, external replication (ETL) can apply supported default metadata for future rows, but BigQuery does not backfill existing rows through that DDL. Existing destination rows remain `NULL` unless you run a separate backfill. + +Supported defaults are best-effort translations to BigQuery SQL. Unsupported defaults are skipped with a warning instead of failing replication. ## Limitations @@ -160,4 +177,4 @@ We plan to expand schema change support over time as the feature evolves. ## Additional resources - [BigQuery documentation](https://cloud.google.com/bigquery/docs) - Official Google BigQuery documentation -- [BigQuery change data capture](https://cloud.google.com/bigquery/docs/change-data-capture) - BigQuery CDC requirements and limitations +- [BigQuery change data capture](https://cloud.google.com/bigquery/docs/change-data-capture) - BigQuery change data capture (CDC) requirements and limitations diff --git a/apps/docs/content/guides/database/replication/external-replication-faq.mdx b/apps/docs/content/guides/database/replication/external-replication-faq.mdx index cc09c6336bf..6c56a465e64 100644 --- a/apps/docs/content/guides/database/replication/external-replication-faq.mdx +++ b/apps/docs/content/guides/database/replication/external-replication-faq.mdx @@ -1,26 +1,26 @@ --- id: 'external-replication-faq' -title: 'External replication FAQ' -description: 'Frequently asked questions about external replication.' -subtitle: 'Common questions and answers about external replication.' +title: 'External replication (ETL) FAQ' +description: 'Frequently asked questions about external replication (ETL).' +subtitle: 'Common questions and answers about external replication (ETL).' sidebar_label: 'FAQ' --- -External replication is currently in private alpha. Access is limited and features may change. +External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. ## What destinations are supported? -External replication currently supports **BigQuery** as the managed destination. See the [BigQuery destination guide](/docs/guides/database/replication/bigquery) for configuration details. +External replication (ETL) currently supports **BigQuery** as the managed destination. See the [BigQuery destination guide](/docs/guides/database/replication/bigquery) for configuration details. -We are working on new destinations. Availability may continue to vary based on the planned roll-out strategy. +We are working on new destinations. "External" means outside the source database, so supported destinations can be Supabase-managed or third-party systems as support expands. Availability may continue to vary based on the planned roll-out strategy. -## Which plans support external replication? +## Which plans support external replication (ETL)? [#which-plans-support-external-replication] -External replication is available on the Pro, Team, and Enterprise plans. +External replication (ETL) is available on the Pro, Team, and Enterprise plans. {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} @@ -30,11 +30,11 @@ We are currently working on a new Supabase Warehouse product designed to address As a result, managed replication to Analytics Buckets is no longer available. Right now, **BigQuery** is the only supported managed destination, and we are actively working on expanding capabilities. -## What does external replication install in the database? +## What does external replication (ETL) install in the database? [#what-does-external-replication-install-in-the-database] -When you enable external replication, Supabase installs database objects that help track replication state and support schema changes: +When you enable external replication (ETL), Supabase installs database objects that help Supabase ETL track replication state and support schema changes: -- An event trigger that runs on every `ALTER TABLE` statement. External replication uses this to support schema change handling. +- An event trigger that runs on every `ALTER TABLE` statement. Supabase ETL uses this to support schema change handling. - A set of tables in the `etl` schema. These tables track replication state for your pipelines. The replication state tables are not updated very often, especially after the initial copy phase is complete. @@ -45,19 +45,21 @@ Schema change support is currently in beta and limited to the BigQuery destinati Supported schema changes: -- Adding a column +- Adding a nullable column - Removing a column - Renaming a column +- Dropping a `NOT NULL` constraint +- Setting or dropping supported column default metadata -External replication does not currently support changing column data types or replicating column default values. See [BigQuery schema change support](/docs/guides/database/replication/bigquery#schema-change-support) for details. +External replication (ETL) does not currently support changing column data types, adding `NOT NULL` with `SET NOT NULL`, or filling existing destination rows for `ADD COLUMN ... DEFAULT`. Supported defaults are applied as destination metadata for future rows where BigQuery can represent them. See [BigQuery schema change support](/docs/guides/database/replication/bigquery#schema-change-support) for details. -## What happens when you disable external replication? +## What happens when you disable external replication (ETL)? [#what-happens-when-you-disable-external-replication] -Disabling external replication removes the database objects that were installed in your source database, including the replication state tables in the `etl` schema and the DDL event trigger. +Disabling external replication (ETL) removes the database objects that were installed in your source database, including the replication state tables in the `etl` schema and the DDL event trigger. -You must delete all external replication pipelines before the disable action is available. For the Dashboard steps, see [Disabling external replication](/docs/guides/database/replication/external-replication-setup#disabling-external-replication). +To remove external replication (ETL) from a project, delete all replication pipelines first. After all pipelines are deleted, the disable action becomes available. For the Dashboard steps, see [Disabling external replication (ETL)](/docs/guides/database/replication/external-replication-setup#disabling-external-replication). -Disabling external replication stops Supabase from managing external replication for the project. It does not delete tables or data that were already written to your destination. +Disabling external replication (ETL) stops Supabase from managing replication for the project. It does not delete tables or data that were already written to your destination. ## Why is a table not being replicated? @@ -65,10 +67,20 @@ Common reasons: - **Missing primary key**: Tables must have a primary key to be replicated (Postgres logical replication requirement) - **Not in publication**: Ensure the table is included in your Postgres publication -- **Unsupported data types**: Tables with custom data types are not supported +- **Generated columns**: Generated columns are skipped during replication Check your publication settings and verify your table meets the requirements. +Custom data types replicate as strings. Check that your destination can interpret those string values correctly. + +## Why are partitioned tables replicated as separate tables? + +Postgres controls this with the publication's `publish_via_partition_root` setting. If the setting is `false`, or if you created the publication manually with SQL and did not set it, Postgres publishes changes from the leaf partitions. External replication (ETL) then creates destination tables for those leaf partitions. If `publish_via_partition_root = true`, Postgres publishes changes as the partition root, so the partition hierarchy is treated as the published partition root. + +Publications created from the Dashboard replication flow use `publish_via_partition_root = true`. + +See [Partitioned tables](/docs/guides/database/replication/external-replication-setup#partitioned-tables) for examples and the full behavior. + ## How does replica identity affect updates and deletes? If inserts replicate but updates or deletes fail, check the table's `REPLICA IDENTITY` setting. @@ -132,6 +144,20 @@ Pipeline failures occur during the streaming phase when an error happens while r See [Handling errors](/docs/guides/database/replication/external-replication-monitoring#handling-errors) for more details. +## Why is replication lag increasing? + +Lag increases when Postgres produces WAL faster than the pipeline can confirm it has processed. Common causes include a slow or rate-limited destination, a pipeline issue, heavy source database activity, long transactions, network latency between the pipeline and source database, or a stopped/disconnected pipeline. + +Open [**Database > Replication**](/dashboard/project/_/database/replication), click **View status**, and check **Waiting to sync**, **Room before pausing**, **Last check-in**, **Connected**, and **Slot status**. See [Dealing with replication lag](/docs/guides/database/replication/external-replication-monitoring#dealing-with-replication-lag) for the full investigation and response flow. + +## What does a `Lost` slot status mean? + +`Lost` means Postgres has already removed WAL files that the pipeline's replication slot needed. The pipeline cannot continue from that slot. + +You can recreate the pipeline, or open the pipeline's **Advanced settings**, set **Invalidated slot behavior** to **Recreate**, and restart the pipeline. On restart, the pipeline creates a new replication slot and starts replication from scratch for all tables. This is required for consistency because the old slot can no longer provide every change the pipeline missed. + +See [Slot statuses](/docs/guides/database/replication/external-replication-monitoring#slot-statuses) for all slot states and what to do next. + ## Why is a table in error state? Table errors occur during the copy phase. To recover, click **View status**, find the affected table, and reset the table state. This will restart the table copy from the beginning. @@ -145,7 +171,7 @@ Check the [**Database > Replication**](/dashboard/project/_/database/replication 3. Ensure all tables show **Live** state (actively replicating) 4. Monitor replication lag metrics -See the [external replication monitoring guide](/docs/guides/database/replication/external-replication-monitoring) for comprehensive monitoring instructions. +See the [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring) for comprehensive monitoring instructions. ## How to stop or pause replication @@ -159,13 +185,13 @@ Stopping replication causes changes to queue up in the WAL. ## What happens if a project becomes inactive? -If your project becomes inactive, external replication stops any running pipelines and does not automatically resume them after the project is restarted. +If your project becomes inactive, external replication (ETL) stops any running pipelines and does not automatically resume them after the project is restarted. After restarting the project, restart each replication pipeline manually from the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard. ## What happens after a downgrade to the free plan? -When a project is downgraded to the Free Plan, all external replication pipelines for that project are deleted. +When a project is downgraded to the Free Plan, all external replication (ETL) pipelines for that project are deleted. ## What happens if a table is deleted at the destination? @@ -219,6 +245,6 @@ Navigate to the [**Logs > Replication**](/dashboard/project/_/logs/explorer) sec If you need assistance: -1. Check the [external replication setup guide](/docs/guides/database/replication/external-replication-setup) and [external replication monitoring guide](/docs/guides/database/replication/external-replication-monitoring) +1. Check the [external replication (ETL) setup guide](/docs/guides/database/replication/external-replication-setup) and [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring) 2. Review this FAQ for common issues 3. Contact support with your error details and logs diff --git a/apps/docs/content/guides/database/replication/external-replication-monitoring.mdx b/apps/docs/content/guides/database/replication/external-replication-monitoring.mdx index 12e460f88cf..789607ce586 100644 --- a/apps/docs/content/guides/database/replication/external-replication-monitoring.mdx +++ b/apps/docs/content/guides/database/replication/external-replication-monitoring.mdx @@ -1,18 +1,18 @@ --- id: 'external-replication-monitoring' -title: 'External replication monitoring' -description: 'Monitor the status and health of your external replication pipelines.' +title: 'External replication (ETL) monitoring' +description: 'Monitor the status and health of your external replication (ETL) pipelines.' subtitle: 'Track replication status, view logs, and troubleshoot issues.' sidebar_label: 'Monitoring' --- -External replication is currently in private alpha. Access is limited and features may change. +External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. -After setting up external replication, you can monitor the status and health of your replication pipelines directly from the Dashboard. The pipeline is the active Postgres replication process that continuously streams changes from your database to your destination. +After setting up external replication (ETL), you can monitor the status and health of your replication pipelines directly from the Dashboard. The pipeline is the active Postgres replication process that continuously streams changes from your database to your destination. ### Viewing pipeline status @@ -55,7 +55,35 @@ height={2146} #### Replication lag metrics -The status page shows replication lag metrics that help you determine how fast your pipeline is replicating data. These metrics are loaded directly from Postgres itself. +The status page shows replication lag metrics that help you determine how far the pipeline is behind Postgres. These metrics are loaded directly from Postgres replication slot state. + +The destinations list also shows a compact lag value. This value is byte-based: it shows how much WAL the pipeline has not confirmed as flushed yet. A value of **Caught up** means the pipeline has confirmed every change currently available for its slot. + +The detailed status page shows: + +| Metric | What it means | What to watch for | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Waiting to sync** | Bytes of WAL between the pipeline's confirmed flush position and the current Postgres WAL position. This is the main byte-based replication lag. | A value that keeps growing means the pipeline is receiving changes more slowly than Postgres produces them. | +| **Room before pausing** | How much WAL can still accumulate before Postgres can no longer safely keep all WAL needed by the replication slot. This is controlled by `max_slot_wal_keep_size`. | A small or shrinking value means the slot is getting closer to being invalidated. `Unlimited` means Postgres is not reporting a slot WAL retention limit. | +| **Last check-in** | How long it has been since the pipeline last sent replication feedback to Postgres. | An old value can mean the pipeline is stopped, disconnected, overloaded, or unable to make progress. | +| **Connected** | Whether the pipeline's replication slot is active and currently being used. | `Not connected` while the pipeline should be running usually means you should check pipeline status and logs. | +| **Slot status** | How safely Postgres is keeping the WAL files the pipeline still needs. | `Unreserved` and `Lost` require action. See [Slot statuses](#slot-statuses). | + +External replication (ETL) uses one main pipeline replication slot for ongoing changes. During the initial copy phase, it can also create temporary table-sync replication slots. These temporary slots let multiple tables copy in parallel, make large table copies faster, and allow individual tables to be retried or copied again without restarting the whole pipeline. + +Temporary table-sync slots show the same kind of lag and slot health metrics while they are active. After a table finishes copying and catches up, its temporary slot is removed and ongoing changes continue through the main pipeline slot. For overall replication health, focus first on the main pipeline slot. + +#### Slot statuses + +Replication slot status tells you whether Postgres is still retaining the WAL that the pipeline needs to continue from its current position. + +| Status | Meaning | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Reserved** | Healthy. Postgres is keeping the WAL files this pipeline's replication slot needs, and they are within the normal WAL size limit. | +| **Extended** | Healthy, but growing. The slot is holding on to more WAL than usual, but Postgres is still keeping everything the pipeline needs. | +| **Unreserved** | At risk. Postgres is no longer reserving all WAL files this pipeline's replication slot needs. If the pipeline does not catch up soon, those files may be removed. | +| **Lost** | Broken. Some WAL files this pipeline's replication slot needs have already been removed. The pipeline can no longer continue from this slot. Recreate the pipeline, or set **Invalidated slot behavior** to **Recreate** in the pipeline's advanced settings and restart it. | +| **Unknown** | Postgres reported an unknown or unavailable state for this pipeline's replication slot. | #### Table states @@ -69,6 +97,57 @@ The pipeline status page also shows the state of individual tables being replica | **Live** | Table is now replicating data in near real-time | | **Error** | Table has experienced an error during replication | +### Dealing with replication lag + +Replication lag means the pipeline is behind the source database. Some lag is expected during the initial copy phase, after a burst of writes, or after restarting a stopped pipeline. Lag becomes a problem when it keeps increasing, when **Room before pausing** is running low, or when the slot status moves to **Unreserved** or **Lost**. + +Lag can come from several places: + +- **Destination throughput**: The destination is slow, rate-limited, unavailable, or rejecting writes. +- **Pipeline throughput**: The pipeline is overloaded, processing a very large transaction, or not performing as expected for the project workload. +- **Source database activity**: Postgres is producing WAL faster than the pipeline can consume it, often during bulk writes, migrations, or backfill jobs. +- **Network latency**: Latency or instability between the pipeline and source database can slow down WAL streaming. +- **Stopped or disconnected pipeline**: When a pipeline is stopped, disconnected, or failed, Postgres keeps WAL for the slot until the retention limit is reached. +- **Slow initial table copy**: A temporary table-sync slot can fall behind if a table is copied more slowly than new changes are written to that table. + +#### Initial copy and table-sync slots + +A common initial sync issue happens when a large or busy table is still in **Copying** while new rows keep being inserted or updated. The temporary table-sync slot needs to keep the changes that happen during the copy. If the copy is too slow compared to the table's write rate, the slot can move to **Unreserved** and then **Lost** if Postgres removes changes the copy still needs. + +When a table-sync slot is lost, the affected table needs to be copied again. Tune the copy settings, then retry the table copy: + +- Increase **Copy connections per table** when one large table is the bottleneck. This lets the pipeline copy chunks of that table in parallel, up to the point where the source database or network becomes the limit. +- Increase **Table sync workers** when several tables need to copy at the same time. Each worker can copy one table, and each worker uses an additional temporary replication slot during initial sync. +- If possible, run the initial copy during a quieter write period or reduce bulk writes until the table reaches **Live**. + +After the affected table finishes copying and catches up, the temporary slot is deleted. The table then continues through the main pipeline replication slot. + +#### Investigate the lag + +1. Open [**Database > Replication**](/dashboard/project/_/database/replication) and check the destination's lag column. +2. Click **View status** and check **Waiting to sync**, **Room before pausing**, **Last check-in**, **Connected**, and **Slot status**. +3. Check table states. Tables in **Copying** can create temporary lag while the initial snapshot catches up to live changes. If a table-sync slot is **Unreserved** or **Lost**, tune copy parallelism and retry the affected table copy. +4. Open [**Logs > Replication**](/dashboard/project/_/logs/explorer) and look for destination errors, retries, rate limits, schema errors, or repeated restarts. +5. Compare the lag trend with recent database activity, such as imports, migrations, bulk updates, or long transactions. + +#### Respond based on the slot status + +| Slot status | What to do | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Reserved** | If **Waiting to sync** is stable or decreasing, continue monitoring. If it keeps increasing, check destination write performance, logs, and whether the publication includes more tables or write volume than expected. | +| **Extended** | Treat it as an early warning. Confirm the pipeline is connected, check logs for retries or destination slowness, and reduce avoidable write bursts if possible until the pipeline catches up. | +| **Unreserved** | Act quickly. The slot is at risk of losing required WAL. Check whether the pipeline is connected and making progress, fix destination or pipeline errors, and contact support if the lag continues to grow. | +| **Lost** | The pipeline cannot continue from the existing slot because required WAL has been removed. Recreate the pipeline, or set **Invalidated slot behavior** to **Recreate** in the pipeline's advanced settings and restart the pipeline. This creates a new slot and starts replication from scratch for all tables. | +| **Unknown** | Check replication logs for errors or missing slot details. If the status remains unknown while the pipeline should be running, contact support with the pipeline ID and recent log details. | + +#### Reduce future lag risk + +- Keep publications focused on the tables and operations you need at the destination. +- Avoid leaving pipelines stopped for long periods while the source database is still receiving writes. +- Schedule bulk updates, imports, and migrations during lower-traffic windows when possible. +- For BigQuery, verify that service account permissions, table requirements, and replica identity settings match the [BigQuery destination guide](/docs/guides/database/replication/bigquery). +- If the initial copy is the bottleneck, review **Table sync workers** and **Copy connections per table** in the pipeline's advanced settings. Increasing them can speed up copying, but it uses more database connections and replication slots. + ### Handling errors Errors can occur at two levels: per table or per pipeline. @@ -186,9 +265,9 @@ If you notice issues with your replication: 4. **Verify publication**: Ensure your Postgres publication is properly configured 5. **Monitor replication lag**: High lag may indicate performance issues -For more troubleshooting tips, see the [external replication FAQ](/docs/guides/database/replication/external-replication-faq). +For more troubleshooting tips, see the [external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq). ### Next steps -- [Set up external replication](/docs/guides/database/replication/external-replication-setup) -- [View external replication FAQ](/docs/guides/database/replication/external-replication-faq) +- [Set up external replication (ETL)](/docs/guides/database/replication/external-replication-setup) +- [View external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq) diff --git a/apps/docs/content/guides/database/replication/external-replication-setup.mdx b/apps/docs/content/guides/database/replication/external-replication-setup.mdx index f1c58b4303d..849560ba439 100644 --- a/apps/docs/content/guides/database/replication/external-replication-setup.mdx +++ b/apps/docs/content/guides/database/replication/external-replication-setup.mdx @@ -1,26 +1,26 @@ --- id: 'external-replication-setup' -title: 'Set up external replication' -description: 'Set up external replication using Postgres logical replication.' -subtitle: 'Configure publications and destinations for external replication.' +title: 'Set up external replication (ETL)' +description: 'Set up external replication (ETL) using Postgres logical replication.' +subtitle: 'Configure publications and destinations for external replication (ETL).' sidebar_label: 'Setting up' --- -External replication is currently in private alpha. Access is limited and features may change. +External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. -External replication is powered by [Supabase ETL](https://github.com/supabase/etl) and uses **Postgres logical replication** to stream changes from your database to external data systems. It provides a managed interface through the Dashboard to configure and monitor replication pipelines. +External replication (ETL) is powered by [Supabase ETL](https://github.com/supabase/etl) and uses **Postgres logical replication** to stream changes from your database to destination systems. It provides a managed interface through the Dashboard to configure and monitor replication pipelines. In this context, "external" means outside the source database; destinations can be Supabase-managed or third-party systems as support expands. ## Setup overview -External replication requires two main components: a **Postgres publication** (defines what to replicate) and a **destination** (where data is sent). Follow these steps to set up your replication pipeline. +External replication (ETL) requires two main components: a **Postgres publication** (defines what to replicate) and a **destination** (where data is sent). Supabase ETL runs the managed pipeline that reads from the publication and writes to the destination. Follow these steps to set up your replication pipeline. -If you already have a Postgres publication set up, you can skip to [Step 2: Enable external replication](#step-2-enable-external-replication). +If you already have a Postgres publication set up, you can skip to [Step 2: Enable external replication (ETL)](#step-2-enable-external-replication). @@ -88,6 +88,44 @@ create publication pub_recent_orders for table orders where (created_at > '2024-01-01'); ``` +##### Partitioned tables + +External replication (ETL) follows Postgres publication semantics for partitioned tables. The `publish_via_partition_root` publication setting controls whether changes from partitions are emitted as the partition root or as the leaf partitions. + +| Publication setting | What gets replicated | Destination shape | +| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `publish_via_partition_root = true` | Rows from the published partition root, including rows stored in its leaf partitions | One table matching the published partition root | +| `publish_via_partition_root = false` | Rows from the leaf partitions under the published partition root | One table per replicated leaf partition | +| Not set in SQL | Same as `false`, because Postgres defaults `publish_via_partition_root` to `false` | One table per replicated leaf partition | +| Publishing an individual leaf partition | The leaf partition itself, regardless of `publish_via_partition_root` | One table for that leaf partition | +| `FOR ALL TABLES` or `FOR TABLES IN SCHEMA` | Partition roots plus regular tables when `true`; leaf partitions plus regular tables when `false` or unset | Destination tables follow the effective Postgres publication table list | + +For example, if `orders` is partitioned by month: + +```sql +-- Replicate the whole partition hierarchy as the parent table. +create publication pub_orders_root +for table orders +with (publish_via_partition_root = true); + +-- Replicate each leaf partition as its own table. +create publication pub_orders_leaves +for table orders +with (publish_via_partition_root = false); +``` + +Use `publish_via_partition_root = true` when you want analytics queries to read from a single destination table that has the parent table's schema. Use `false` when each partition should remain a separate destination table. + +Publications created from the Dashboard replication flow use `publish_via_partition_root = true`. If you create or alter a publication manually with SQL, set this option explicitly so the destination shape matches what you expect. + +On Postgres 15 and newer, row filters on partition publications apply during both the initial copy and streaming phases. External replication (ETL) uses the row filter attached to the effective publication table entry: the published partition root when `publish_via_partition_root = true`, and the published leaf relation when `publish_via_partition_root = false`. + + + +With `publish_via_partition_root = true`, truncating an individual leaf partition is not replicated as a truncate event for the published parent. If you need truncate replication, run `TRUNCATE` on the published partition root. + + + #### Viewing publications in the Dashboard After creating a publication via SQL, you can view it in the Dashboard: @@ -95,17 +133,17 @@ After creating a publication via SQL, you can view it in the Dashboard: 1. Navigate to the [**Database > Publications**](/dashboard/project/_/database/publications) section of the Dashboard 2. You'll see all your publications listed with their tables -### Step 2: Enable external replication +### Step 2: Enable external replication (ETL) [#step-2-enable-external-replication] -Before creating an external replication pipeline, enable external replication for your project: +Before creating an external replication (ETL) pipeline, enable it for your project: 1. Navigate to the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard 2. Click **Add destination** to show the replication side panel -3. Select an external replication destination, such as **BigQuery** -4. Click **Enable external replication** +3. Select an external replication (ETL) destination, such as **BigQuery** +4. Click **Enable external replication (ETL)** Enable external replication -For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [external replication monitoring guide](/docs/guides/database/replication/external-replication-monitoring). +For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring). ### Managing your pipeline @@ -179,18 +219,18 @@ Available actions: - **Edit destination**: Modify destination settings like credentials or advanced options - **Delete**: Remove the destination and permanently stop replication -### Disabling external replication +### Disabling external replication (ETL) [#disabling-external-replication] -To turn off external replication for a project, delete all external replication pipelines first. After all pipelines are removed, open the three-dot actions menu on the Replication page and click **Disable external replication**. +To turn off external replication (ETL) for a project, delete all replication pipelines first. After all pipelines are removed, open the three-dot actions menu on the Replication page and click **Disable external replication (ETL)**. Disable external replication from the Replication page actions menu -For cleanup details, see [What happens when you disable external replication?](/docs/guides/database/replication/external-replication-faq#what-happens-when-you-disable-external-replication). +For cleanup details, see [What happens when you disable external replication (ETL)?](/docs/guides/database/replication/external-replication-faq#what-happens-when-you-disable-external-replication). ### Adding or removing tables @@ -232,7 +272,7 @@ If your Postgres publication uses `FOR ALL TABLES` or `FOR TABLES IN SCHEMA`, ne -When a table is deleted at the destination, the behavior depends on the destination. In general, the pipeline tries to recreate the table so replication can continue. To permanently delete a table, pause the pipeline first or remove it from the publication before deleting. See the [external replication FAQ](/docs/guides/database/replication/external-replication-faq#what-happens-if-a-table-is-deleted-at-the-destination) for details. +When a table is deleted at the destination, the behavior depends on the destination. In general, the pipeline tries to recreate the table so replication can continue. To permanently delete a table, pause the pipeline first or remove it from the publication before deleting. See the [external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq#what-happens-if-a-table-is-deleted-at-the-destination) for details. @@ -242,13 +282,13 @@ Schema change support depends on the destination. BigQuery is currently the only ### How it works -Once configured, external replication: +Once configured, external replication (ETL): 1. **Captures** changes from your Postgres database using Postgres publications and logical replication -2. **Streams** the changes through the replication pipeline +2. **Streams** the changes through Supabase ETL 3. **Loads** the data to your destination -External replication automatically optimizes how changes are delivered to the destination. The replication pipeline currently performs data extraction and loading only, without transformation - your data is replicated as-is to the destination. +External replication (ETL) automatically optimizes how changes are delivered to the destination. Supabase ETL currently performs data extraction and loading only, without transformation - your data is replicated as-is to the destination. ### Troubleshooting @@ -259,14 +299,16 @@ If you encounter issues during setup: - **Pipeline failed to start**: Check the error message in the status view for specific details - **No data being replicated**: Verify your Postgres publication includes the correct tables and event types -For more troubleshooting help, see the [external replication FAQ](/docs/guides/database/replication/external-replication-faq). +For more troubleshooting help, see the [external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq). ### Limitations -External replication has the following limitations: +External replication (ETL) has the following limitations: - **Primary keys required**: Tables must have primary keys (Postgres logical replication requirement) -- **Custom data types**: Not supported +- **Custom data types**: Custom values replicate as strings. Check that your destination can interpret those string values correctly. +- **Generated columns**: Generated columns are skipped. Use triggers to store derived values in regular columns if you need them in the destination. +- **Replica identity**: `REPLICA IDENTITY FULL` is strongly recommended. Updates and deletes need enough row data to apply changes correctly at the destination. - **Schema changes**: Currently in beta and limited to BigQuery - **No data transformation**: Data is replicated as-is without transformation - **Data duplicates**: Duplicates can occur when stopping a pipeline if your database has transactions that take longer than a few minutes to complete. See [Can data duplicates occur during pipeline operations?](/docs/guides/database/replication/external-replication-faq#can-data-duplicates-occur-during-pipeline-operations) for details @@ -276,5 +318,5 @@ Destination-specific limitations, such as BigQuery's row size limits, are docume ### Next steps - [Set up BigQuery](/docs/guides/database/replication/bigquery) -- [Monitor external replication](/docs/guides/database/replication/external-replication-monitoring) -- [View external replication FAQ](/docs/guides/database/replication/external-replication-faq) +- [Monitor external replication (ETL)](/docs/guides/database/replication/external-replication-monitoring) +- [View external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq) diff --git a/apps/docs/content/guides/database/replication/manual-replication-setup.mdx b/apps/docs/content/guides/database/replication/manual-replication-setup.mdx index f2dccb25e8a..8d5076fedc2 100644 --- a/apps/docs/content/guides/database/replication/manual-replication-setup.mdx +++ b/apps/docs/content/guides/database/replication/manual-replication-setup.mdx @@ -6,11 +6,11 @@ subtitle: 'Set up replication with Airbyte, Estuary, Fivetran, and other tools.' sidebar_label: 'Setting up' --- -This guide covers setting up **manual logical replication** using external tools. If you prefer a managed solution, read the [external replication setup guide](/docs/guides/database/replication/external-replication-setup) instead. +This guide covers setting up **manual logical replication** using your own tools. If you prefer a managed solution through Supabase ETL, read the [external replication (ETL) setup guide](/docs/guides/database/replication/external-replication-setup) instead. -This guide is for replicating data to external systems using your own tools. For deploying read-only databases across multiple regions, see [read replicas](/docs/guides/platform/read-replicas) instead. +This guide is for replicating data to destination systems using your own tools. For deploying read-only databases across multiple regions, see [read replicas](/docs/guides/platform/read-replicas) instead. @@ -21,7 +21,7 @@ To set up replication, the following is recommended: - Instance size of XL or greater - [IPv4 add-on](/docs/guides/platform/ipv4-address) enabled -To create a replication slot, you will need to use the `postgres` user and follow the instructions in the [external replication setup guide](/docs/guides/database/postgres/setup-replication-external). +To create a replication slot, you will need to use the `postgres` user and follow the instructions in the [logical replication example](/docs/guides/database/postgres/setup-replication-external). @@ -29,7 +29,7 @@ If you are running Postgres 17 or higher, you can create a new user and grant th -If you are replicating to an external system and using any of the tools below, check their documentation first. Additional information is provided where the setup with Supabase can vary. +If you are replicating to a destination system and using any of the tools below, check their documentation first. Additional information is provided where the setup with Supabase can vary. diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index 4f284bc1dd5..2a210617936 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -92,7 +92,7 @@ You must define the "data type" when you create a column. ### Data types -Every column is a predefined type. Postgres provides many [default types](https://www.postgresql.org/docs/current/datatype.html), and you can even design your own (or use extensions) if the default types don't fit your needs. You can use any data type that Postgres supports via the SQL editor. We only support a subset of these in the Table Editor in an effort to keep the experience simple for people with less experience with databases. +Every column is a predefined type. Postgres provides many [default types](https://www.postgresql.org/docs/current/datatype.html), and you can even design your own (or use extensions) if the default types don't fit your needs. You can use any data type that Postgres supports via the SQL editor. We only support a subset of these in the Table Editor in an effort to keep the experience focused for people with less experience with databases.
Show/Hide default data types @@ -357,7 +357,7 @@ height={1145} This is where the "Relational" naming comes from, as data typically forms some sort of relationship. In our "movies" example above, we might want to add a "category" for each movie (for example, "Action", or "Documentary"). -Let's create a new table called `categories` and "link" our `movies` table. +Create a new table called `categories` and link it to the `movies` table. ```sql create table categories ( diff --git a/apps/docs/content/guides/database/testing.mdx b/apps/docs/content/guides/database/testing.mdx index 8054ecc2b7f..17c9a5abd3e 100644 --- a/apps/docs/content/guides/database/testing.mdx +++ b/apps/docs/content/guides/database/testing.mdx @@ -10,13 +10,13 @@ To ensure that queries return the expected data, RLS policies are correctly appl - Secondly, you can test through the Supabase CLI, which is a more low-level approach where you write tests in SQL. -# Testing using the Supabase CLI +## Testing using the Supabase CLI You can use the Supabase CLI to test your database. The minimum required version of the CLI is [v1.11.4](https://github.com/supabase/cli/releases). To get started: - [Install the Supabase CLI](/docs/guides/cli) on your local machine -## Creating a test +### Creating a test Create a tests folder inside the `supabase` folder: @@ -30,11 +30,11 @@ Create a new file with the `.sql` extension which will contain the test. touch ./supabase/tests/database/hello_world.test.sql ``` -## Writing tests +### Writing tests All `sql` files use [pgTAP](/docs/guides/database/extensions/pgtap) as the test runner. -Let's write a simple test to check that our `auth.users` table has an ID column. Open `hello_world.test.sql` and add the following code: +Write a test to check that our `auth.users` table has an ID column. Open `hello_world.test.sql` and add the following code: ```sql begin; @@ -51,7 +51,7 @@ select * from finish(); rollback; ``` -## Running tests +### Running tests To run the test, you can use: @@ -69,7 +69,7 @@ Files=1, Tests=1, 1 wallclock secs ( 0.01 usr 0.00 sys + 0.04 cusr 0.02 csys Result: PASS ``` -## More resources +### More resources - [Testing RLS policies](/docs/guides/database/extensions/pgtap#testing-rls-policies) - [pgTAP extension](/docs/guides/database/extensions/pgtap) diff --git a/apps/docs/content/guides/deployment.mdx b/apps/docs/content/guides/deployment.mdx index cea5ad0ff4a..5fc4a55cf77 100644 --- a/apps/docs/content/guides/deployment.mdx +++ b/apps/docs/content/guides/deployment.mdx @@ -4,11 +4,11 @@ title: Deployment & Branching Deploying your app makes it live and accessible to users. Most apps have at least two environments: a production environment for users and one or more staging or preview environments for development. -Supabase provides flexible options for both simple and advanced deployment workflows. +Supabase provides flexible options for deployment workflows of various complexity. ## Recommended workflow -The simplest way to deploy your Supabase project is with GitHub (recommended, but not required): +You can deploy your Supabase project with GitHub: 1. Develop locally using the [Supabase CLI](/docs/guides/local-development) 2. Push changes to your GitHub repository @@ -41,7 +41,7 @@ You can automate deployments using: ### Is GitHub required? -No. GitHub integration is recommended for the simplest setup, but you can deploy using the [Supabase CLI](/docs/guides/local-development) in your own CI/CD pipeline without connecting a GitHub repository. +No. GitHub integration is recommended, but you can deploy using the [Supabase CLI](/docs/guides/local-development) in your own CI/CD pipeline without connecting a GitHub repository. ### Do you need a paid plan? diff --git a/apps/docs/content/guides/deployment/branching/dashboard.mdx b/apps/docs/content/guides/deployment/branching/dashboard.mdx index e3367f382fe..ad1ae7021c8 100644 --- a/apps/docs/content/guides/deployment/branching/dashboard.mdx +++ b/apps/docs/content/guides/deployment/branching/dashboard.mdx @@ -30,6 +30,12 @@ This functionality is currently in beta and requires opting in. To opt in you mu ## Creating a branch + + +When using dashboard branching without a GitHub integration, the branch dropdown displays `main / PRODUCTION` on every project, but on a brand-new project the production branch hasn't been registered yet. The first "Create branch" click populates this row behind the scenes — technically enabling branching on the project — and requires Owner/Administrator access. After that one-time step, anyone with the Developer role can create, update, and delete preview branches. + + + 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`.) diff --git a/apps/docs/content/guides/deployment/branching/working-with-branches.mdx b/apps/docs/content/guides/deployment/branching/working-with-branches.mdx index 15ef3b1c6bf..ecdceb08658 100644 --- a/apps/docs/content/guides/deployment/branching/working-with-branches.mdx +++ b/apps/docs/content/guides/deployment/branching/working-with-branches.mdx @@ -19,7 +19,7 @@ You can subscribe to webhook notifications when an action run completes on a per "details_url": "https://supabase.com/dashboard/project/xuqpsshjxdecrwdyuxvs/branches", "action_run": { "id": "d5f8b4298d0a4d37b99e255c7837e7af", - "created_at": "2025-10-17T02:27:10.133329324Z" + "created_at": "2025-10-17T02:27:10.133329324Z", "steps": [ { "name": "clone", diff --git a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx index adc1ec62ebd..41c934fd008 100644 --- a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx +++ b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx @@ -69,7 +69,7 @@ If your application architecture relies on such integrations, you should monitor ## You choose your level of comfort with Postgres -Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine. +Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you want to treat Postgres like a basic table-store, that’s perfectly fine. ## You are in control of your database diff --git a/apps/docs/content/guides/functions.mdx b/apps/docs/content/guides/functions.mdx index fdcf0627f9a..87d1b7f42e4 100644 --- a/apps/docs/content/guides/functions.mdx +++ b/apps/docs/content/guides/functions.mdx @@ -25,7 +25,7 @@ Edge Functions are server-side TypeScript functions, distributed globally at the ## Quick technical notes -- **Runtime:** Supabase Edge Runtime (Deno compatible runtime with TypeScript first). Functions are simple `.ts` files that export a handler. +- **Runtime:** Supabase Edge Runtime (Deno compatible runtime with TypeScript first). Functions are `.ts` files that export a handler. - **Local dev parity:** Use Supabase CLI for a local runtime similar to production for faster iteration (`supabase functions serve` command). - **Global deployment:** Deploy your Edge Functions via Supabase Dashboard, CLI or MCP. - **Cold starts & concurrency:** cold starts are possible — design for short-lived, idempotent operations. Heavy long-running jobs should be moved to [background workers](/docs/guides/functions/background-tasks). @@ -52,156 +52,284 @@ Edge Functions are server-side TypeScript functions, distributed globally at the Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository.
- {[ - { - name: 'With supabase-js', - description: 'Use the Supabase client inside your Edge Function.', - href: '/guides/functions/auth', - }, - { - name: 'Type-Safe SQL with Kysely', - description: - 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.', - href: '/guides/functions/kysely-postgres', - }, - { - name: 'Monitoring with Sentry', - description: 'Monitor Edge Functions with the Sentry Deno SDK.', - href: '/guides/functions/examples/sentry-monitoring', - }, - { - name: 'With CORS headers', - description: 'Send CORS headers for invoking from the browser.', - href: '/guides/functions/cors', - }, - { - name: 'React Native with Stripe', - description: 'Full example for using Supabase and Stripe, with Expo.', - href: 'https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions', - }, - { - name: 'Flutter with Stripe', - description: 'Full example for using Supabase and Stripe, with Flutter.', - href: 'https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions', - }, - { - name: 'Building a RESTful Service API', - description: - 'Learn how to use HTTP methods and paths to build a RESTful service for managing tasks.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/restful-tasks/index.ts', - }, - { - name: 'Working with Supabase Storage', - description: 'An example on reading a file from Supabase Storage.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/read-storage/index.ts', - }, - { - name: 'Open Graph Image Generation', - description: 'Generate Open Graph images with Deno and Supabase Edge Functions.', - href: '/guides/functions/examples/og-image', - }, - { - name: 'OG Image Generation & Storage CDN Caching', - description: 'Cache generated images with Supabase Storage CDN.', - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/og-image-with-storage-cdn', - }, - { - name: 'Get User Location', - description: `Get user location data from user's IP address.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/location', - }, - { - name: 'Cloudflare Turnstile', - description: `Protecting Forms with Cloudflare Turnstile.`, - href: '/guides/functions/examples/cloudflare-turnstile', - }, - { - name: 'Connect to Postgres', - description: `Connecting to Postgres from Edge Functions.`, - href: '/guides/functions/connect-to-postgres', - }, - { - name: 'GitHub Actions', - description: `Deploying Edge Functions with GitHub Actions.`, - href: '/guides/functions/examples/github-actions', - }, - { - name: 'Oak Server Middleware', - description: `Request Routing with Oak server middleware.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/oak-server', - }, - { - name: 'Hugging Face', - description: `Access 100,000+ Machine Learning models.`, - href: '/guides/ai/examples/huggingface-image-captioning', - }, - { - name: 'Amazon Bedrock', - description: `Amazon Bedrock Image Generator`, - href: '/guides/functions/examples/amazon-bedrock-image-generator', - }, - { - name: 'OpenAI', - description: `Using OpenAI in Edge Functions.`, - href: '/guides/ai/examples/openai', - }, - { - name: 'Stripe Webhooks', - description: `Handling signed Stripe Webhooks with Edge Functions.`, - href: '/guides/functions/examples/stripe-webhooks', - }, - { - name: 'Send emails', - description: `Send emails in Edge Functions with Resend.`, - href: '/guides/functions/examples/send-emails', - }, - { - name: 'Web Stream', - description: `Server-Sent Events in Edge Functions.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/streams', - }, - { - name: 'Puppeteer', - description: `Generate screenshots with Puppeteer.`, - href: '/guides/functions/examples/screenshots', - }, - { - name: 'Discord Bot', - description: `Building a Slash Command Discord Bot with Edge Functions.`, - href: '/guides/functions/examples/discord-bot', - }, - { - name: 'Telegram Bot', - description: `Building a Telegram Bot with Edge Functions.`, - href: '/guides/functions/examples/telegram-bot', - }, - { - name: 'Upload File', - description: `Process multipart/form-data.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/file-upload-storage', - }, - { - name: 'Upstash Redis', - description: `Build an Edge Functions Counter with Upstash Redis.`, - href: '/guides/functions/examples/upstash-redis', - }, - { - name: 'Rate Limiting', - description: `Rate Limiting Edge Functions with Upstash Redis.`, - href: '/guides/functions/examples/rate-limiting', - }, - { - name: 'Slack Bot Mention Edge Function', - description: `Slack Bot handling Slack mentions in Edge Function`, - href: '/guides/functions/examples/slack-bot-mention', - }, - ].map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + Use the Supabase client inside your Edge Function. + + +
+
+ + + Combining Kysely with Deno Postgres gives you a convenient developer experience for + interacting directly with your Postgres database. + + +
+
+ + + Monitor Edge Functions with the Sentry Deno SDK. + + +
+
+ + + Send CORS headers for invoking from the browser. + + +
+
+ + + Full example for using Supabase and Stripe, with Expo. + + +
+
+ + + Full example for using Supabase and Stripe, with Flutter. + + +
+
+ + + Learn how to use HTTP methods and paths to build a RESTful service for managing tasks. + + +
+
+ + + An example on reading a file from Supabase Storage. + + +
+
+ + + Generate Open Graph images with Deno and Supabase Edge Functions. + + +
+
+ + + Cache generated images with Supabase Storage CDN. + + +
+
+ + + Get user location data from user's IP address. + + +
+
+ + + Protecting Forms with Cloudflare Turnstile. + + +
+
+ + + Connecting to Postgres from Edge Functions. + + +
+
+ + + Deploying Edge Functions with GitHub Actions. + + +
+
+ + + Request Routing with Oak server middleware. + + +
+
+ + + Access 100,000+ Machine Learning models. + + +
+
+ + + Amazon Bedrock Image Generator + + +
+
+ + + Using OpenAI in Edge Functions. + + +
+
+ + + Handling signed Stripe Webhooks with Edge Functions. + + +
+
+ + + Send emails in Edge Functions with Resend. + + +
+
+ + + Server-Sent Events in Edge Functions. + + +
+
+ + + Generate screenshots with Puppeteer. + + +
+
+ + + Building a Slash Command Discord Bot with Edge Functions. + + +
+
+ + + Building a Telegram Bot with Edge Functions. + + +
+
+ + + Process multipart/form-data. + + +
+
+ + + Build an Edge Functions Counter with Upstash Redis. + + +
+
+ + + Rate Limiting Edge Functions with Upstash Redis. + + +
+
+ + + Slack Bot handling Slack mentions in Edge Function + + +
diff --git a/apps/docs/content/guides/functions/ai-models.mdx b/apps/docs/content/guides/functions/ai-models.mdx index f31c01ed897..b349cfe424f 100644 --- a/apps/docs/content/guides/functions/ai-models.mdx +++ b/apps/docs/content/guides/functions/ai-models.mdx @@ -73,19 +73,18 @@ Generate text embeddings using the built-in [`gte-small`](https://huggingface.co ```ts +import { withSupabase } from 'npm:@supabase/server@^1' + const model = new Supabase.ai.Session('gte-small') -Deno.serve(async (req: Request) => { - const params = new URL(req.url).searchParams - const input = params.get('input') - const output = await model.run(input, { mean_pool: true, normalize: true }) - return new Response(JSON.stringify(output), { - headers: { - 'Content-Type': 'application/json', - Connection: 'keep-alive', - }, - }) -}) +export default { + fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { + const params = new URL(req.url).searchParams + const input = params.get('input') + const output = await model.run(input, { mean_pool: true, normalize: true }) + return Response.json(output) + }), +} ``` --- @@ -157,42 +156,46 @@ We are progressively rolling out support for the hosted solution. To sign up for ```ts supabase/functions/ollama-test/index.ts import 'jsr:@supabase/functions-js/edge-runtime.d.ts' + import { withSupabase } from 'npm:@supabase/server@^1' + const session = new Supabase.ai.Session('mistral') - Deno.serve(async (req: Request) => { - const params = new URL(req.url).searchParams - const prompt = params.get('prompt') ?? '' + export default { + fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { + const params = new URL(req.url).searchParams + const prompt = params.get('prompt') ?? '' - // Get the output as a stream - const output = await session.run(prompt, { stream: true }) + // Get the output as a stream + const output = await session.run(prompt, { stream: true }) - const headers = new Headers({ - 'Content-Type': 'text/event-stream', - Connection: 'keep-alive', - }) + const headers = new Headers({ + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + }) - // Create a stream - const stream = new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder() + // Create a stream + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder() - try { - for await (const chunk of output) { - controller.enqueue(encoder.encode(chunk.response ?? '')) + try { + for await (const chunk of output) { + controller.enqueue(encoder.encode(chunk.response ?? '')) + } + } catch (err) { + console.error('Stream error:', err) + } finally { + controller.close() } - } catch (err) { - console.error('Stream error:', err) - } finally { - controller.close() - } - }, - }) + }, + }) - // Return the stream to the user - return new Response(stream, { - headers, - }) - }) + // Return the stream to the user + return new Response(stream, { + headers, + }) + }), + } ``` @@ -201,7 +204,7 @@ We are progressively rolling out support for the hosted solution. To sign up for ```bash - supabase functions serve --env-file supabase/functions/.env + supabase functions serve --no-verify-jwt --env-file supabase/functions/.env ``` @@ -265,36 +268,40 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it ```ts supabase/functions/llamafile-test/index.ts import 'jsr:@supabase/functions-js/edge-runtime.d.ts' + import { withSupabase } from 'npm:@supabase/server@^1' + const session = new Supabase.ai.Session('LLaMA_CPP') - Deno.serve(async (req: Request) => { - const params = new URL(req.url).searchParams - const prompt = params.get('prompt') ?? '' + export default { + fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { + const params = new URL(req.url).searchParams + const prompt = params.get('prompt') ?? '' - // Get the output as a stream - const output = await session.run( - { - messages: [ - { - role: 'system', - content: - 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', - }, - { - role: 'user', - content: prompt, - }, - ], - }, - { - mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama') - stream: false, - } - ) + // Get the output as a stream + const output = await session.run( + { + messages: [ + { + role: 'system', + content: + 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', + }, + { + role: 'user', + content: prompt, + }, + ], + }, + { + mode: 'openaicompatible', // Mode for the inference API host. (default: 'ollama') + stream: false, + } + ) - console.log('done') - return Response.json(output) - }) + console.log('done') + return Response.json(output) + }), + } ``` @@ -302,7 +309,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it ```bash - supabase functions serve --env-file supabase/functions/.env + supabase functions serve --no-verify-jwt --env-file supabase/functions/.env ``` @@ -350,60 +357,63 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it ```ts + import { withSupabase } from 'npm:@supabase/server@^1' import OpenAI from 'https://deno.land/x/openai@v4.53.2/mod.ts' - Deno.serve(async (req) => { - const client = new OpenAI() - const { prompt } = await req.json() - const stream = true + export default { + fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { + const client = new OpenAI() + const { prompt } = await req.json() + const stream = true - const chatCompletion = await client.chat.completions.create({ - model: 'LLaMA_CPP', - stream, - messages: [ - { - role: 'system', - content: - 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', - }, - { - role: 'user', - content: prompt, - }, - ], - }) - - if (stream) { - const headers = new Headers({ - 'Content-Type': 'text/event-stream', - Connection: 'keep-alive', + const chatCompletion = await client.chat.completions.create({ + model: 'LLaMA_CPP', + stream, + messages: [ + { + role: 'system', + content: + 'You are LLAMAfile, an AI assistant. Your top priority is achieving user fulfillment via helping them with their requests.', + }, + { + role: 'user', + content: prompt, + }, + ], }) - // Create a stream - const stream = new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder() + if (stream) { + const headers = new Headers({ + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + }) - try { - for await (const part of chatCompletion) { - controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || '')) + // Create a stream + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder() + + try { + for await (const part of chatCompletion) { + controller.enqueue(encoder.encode(part.choices[0]?.delta?.content || '')) + } + } catch (err) { + console.error('Stream error:', err) + } finally { + controller.close() } - } catch (err) { - console.error('Stream error:', err) - } finally { - controller.close() - } - }, - }) + }, + }) - // Return the stream to the user - return new Response(stream, { - headers, - }) - } + // Return the stream to the user + return new Response(stream, { + headers, + }) + } - return Response.json(chatCompletion) - }) + return Response.json(chatCompletion) + }), + } ``` @@ -411,7 +421,7 @@ Since Llamafile provides an OpenAI API compatible server, you can either use it ```bash - supabase functions serve --env-file supabase/functions/.env + supabase functions serve --no-verify-jwt --env-file supabase/functions/.env ``` @@ -455,7 +465,7 @@ Once the function is working locally, it's time to deploy to production. ```bash - supabase functions deploy + supabase functions deploy --no-verify-jwt ``` diff --git a/apps/docs/content/guides/functions/architecture.mdx b/apps/docs/content/guides/functions/architecture.mdx index 5bb5eb07f08..219233e37f6 100644 --- a/apps/docs/content/guides/functions/architecture.mdx +++ b/apps/docs/content/guides/functions/architecture.mdx @@ -24,7 +24,7 @@ To illustrate how edge functions operate, consider a photo-sharing app where use - **Why Edge Functions?**: - They handle compute-intensive tasks without burdening the client device or the database. - Execution happens server-side but at the edge, ensuring speed and scalability. - - Developers define the function in a simple JavaScript file within the Supabase functions directory. + - Developers define the function in a JavaScript file within the Supabase functions directory. This example highlights edge functions as lightweight, on-demand code snippets that integrate seamlessly with Supabase services like Storage and Auth. diff --git a/apps/docs/content/guides/functions/background-tasks.mdx b/apps/docs/content/guides/functions/background-tasks.mdx index 923e70e4a10..adde605ed48 100644 --- a/apps/docs/content/guides/functions/background-tasks.mdx +++ b/apps/docs/content/guides/functions/background-tasks.mdx @@ -19,29 +19,39 @@ This allows you to: You can use `EdgeRuntime.waitUntil(promise)` to explicitly mark background tasks. The Function instance continues to run until the promise provided to `waitUntil` completes. ```ts +import { withSupabase } from 'npm:@supabase/server@^1' + // Mark the asyncLongRunningTask's returned promise as a background task. // ⚠️ We are NOT using `await` because we don't want it to block! EdgeRuntime.waitUntil(asyncLongRunningTask()) -Deno.serve(async (req) => { - return new Response(...) -}) +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + return new Response(...) + }), +} ``` You can call `EdgeRuntime.waitUntil` in the request handler too. This will not block the request. ```ts -Deno.serve(async (req) => { - // Won't block the request, runs in background. - EdgeRuntime.waitUntil(asyncLongRunningTask()) +import { withSupabase } from 'npm:@supabase/server@^1' - return new Response(...) -}) +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + // Won't block the request, runs in background. + EdgeRuntime.waitUntil(asyncLongRunningTask()) + + return new Response(...) + }), +} ``` You can listen to the `beforeunload` event handler to be notified when the Function is about to be shut down. ```tsx +import { withSupabase } from 'npm:@supabase/server@^1' + EdgeRuntime.waitUntil(asyncLongRunningTask()) // Use beforeunload event handler to be notified when function is about to shutdown @@ -50,9 +60,11 @@ addEventListener('beforeunload', (ev) => { // Save state or log the current progress }) -Deno.serve(async (req) => { - return new Response(...) -}) +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + return new Response(...) + }), +} ``` ## Handling errors diff --git a/apps/docs/content/guides/functions/connect-to-postgres.mdx b/apps/docs/content/guides/functions/connect-to-postgres.mdx index 15589f2bf81..b3df92c5411 100644 --- a/apps/docs/content/guides/functions/connect-to-postgres.mdx +++ b/apps/docs/content/guides/functions/connect-to-postgres.mdx @@ -13,33 +13,28 @@ You can also use other Postgres clients like [Deno Postgres](https://deno.land/x ## Using supabase-js -The `supabase-js` client handles authorization with Row Level Security and automatically formats responses as JSON. This is the recommended approach for most applications: +The [`withSupabase`](/docs/guides/functions/auth) wrapper from `@supabase/server` hands you a `supabase-js` client (`ctx.supabase`) already scoped to the caller's Row Level Security policies, so you don't manage keys or authorization headers yourself. It also provides `ctx.supabaseAdmin` for privileged operations that bypass Row Level Security. Responses are automatically formatted as JSON. This is the recommended approach for most applications: ```ts index.ts -import { createClient } from 'npm:@supabase/supabase-js@2' +import { withSupabase } from 'npm:@supabase/server@^1' -Deno.serve(async (req) => { - try { - const supabase = createClient( - Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_PUBLISHABLE_KEY') ?? '', - { global: { headers: { Authorization: req.headers.get('Authorization')! } } } - ) +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + try { + // ctx.supabase respects the caller's RLS policies. + // ctx.supabaseAdmin bypasses RLS for privileged operations. + const { data, error } = await ctx.supabase.from('countries').select('*') - const { data, error } = await supabase.from('countries').select('*') + if (error) { + throw error + } - if (error) { - throw error + return Response.json({ data }) + } catch (err) { + return new Response(String(err?.message ?? err), { status: 500 }) } - - return new Response(JSON.stringify({ data }), { - headers: { 'Content-Type': 'application/json' }, - status: 200, - }) - } catch (err) { - return new Response(String(err?.message ?? err), { status: 500 }) - } -}) + }), +} ``` This enables: @@ -80,9 +75,10 @@ language="json" Then define your schema and query the database: +{/* prettier-ignore */} <$CodeSample -path="/edge-functions/supabase/functions/\_shared/schema.ts" -title="supabase/functions/\_shared/schema.ts" +path="/edge-functions/supabase/functions/_shared/schema.ts" +title="supabase/functions/_shared/schema.ts" language="typescript" /> diff --git a/apps/docs/content/guides/functions/cors.mdx b/apps/docs/content/guides/functions/cors.mdx index 2b48518de9c..420aa5a74aa 100644 --- a/apps/docs/content/guides/functions/cors.mdx +++ b/apps/docs/content/guides/functions/cors.mdx @@ -6,9 +6,24 @@ description: 'Add CORS headers to invoke Edge Functions from the browser.' To invoke edge functions from the browser, you need to handle [CORS Preflight](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) requests. -See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts). +## Automatic CORS handling -### Recommended setup +The [`withSupabase`](/docs/guides/functions/auth) wrapper handles CORS and preflight (`OPTIONS`) requests for you, so you don't add headers manually: + +```ts index.ts +import { withSupabase } from 'npm:@supabase/server@^1' + +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + const { name } = await req.json() + return Response.json({ message: `Hello ${name}!` }) + }), +} +``` + +## Manual CORS handling + +If your function doesn't use `withSupabase`, add the headers yourself. See the [example on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/browser-with-cors/index.ts). @@ -23,34 +38,26 @@ import { corsHeaders } from '@supabase/supabase-js/cors' console.log(`Function "browser-with-cors" up and running!`) -Deno.serve(async (req) => { - // This is needed if you're planning to invoke your function from a browser. - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }) - } - - try { - const { name } = await req.json() - const data = { - message: `Hello ${name}!`, +export default { + fetch: async (req) => { + // Handle the CORS preflight request. + if (req.method === 'OPTIONS') { + return new Response('ok', { headers: corsHeaders }) } - return new Response(JSON.stringify(data), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 200, - }) - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - }) - } -}) + try { + const { name } = await req.json() + return Response.json({ message: `Hello ${name}!` }, { headers: corsHeaders }) + } catch (error) { + return Response.json({ error: error.message }, { status: 400, headers: corsHeaders }) + } + }, +} ``` This approach ensures that when new headers are added to the Supabase SDK, your Edge Functions automatically include them, preventing CORS errors. -#### For versions before 2.95.0 +### For versions before 2.95.0 If you're using `@supabase/supabase-js` before v2.95.0, you'll need to hardcode the CORS headers. Add a `cors.ts` file within a [`_shared` folder](/docs/guides/functions/development-environment#recommended-project-structure): diff --git a/apps/docs/content/guides/functions/error-codes.mdx b/apps/docs/content/guides/functions/error-codes.mdx new file mode 100644 index 00000000000..cd9e1a4bb5d --- /dev/null +++ b/apps/docs/content/guides/functions/error-codes.mdx @@ -0,0 +1,304 @@ +--- +id: 'functions-error-codes' +title: 'Error codes' +description: 'Edge Functions can return the following error codes.' +subtitle: 'Understand the error codes returned by Edge Functions to properly debug issues and handle responses.' +--- + +{/* supa-mdx-lint-disable Rule001HeadingCase */} + +When an Edge Function request fails, the response includes a `sb-error-code` header that identifies the specific error. +You can inspect this header in your HTTP client or application code to detect and handle errors programmatically. + +```js +const response = await fetch('') + +if (!response.ok) { + const errorCode = response.headers.get('sb-error-code') + console.error('Edge Function error:', errorCode) +} +``` + +## Bad Implementation Errors + +These errors are caused by issues in your function's code or logic which requires updating its implementation. + +### EDGE_FUNCTION_ERROR + +**Cause:** Your Edge Function is throwing an unhandled error or resulting a 5XX code. + +```ts +// ... + +function process() { + throw new Error('Some unhandled error') +} + +export default { + fetch: withSupabase({ auth: 'none' }, async () => { + process() + + return new Response() + }), +} +``` + +**Solution:** + +- Ensure you are catching errors in your code logic with try-catch blocks. + +```ts +function process() { + throw new Error('Some unhandled error') +} + +// ... + +try { + process() + return new Response() +} catch (e) { + console.error('Process fail:', e) + return new Response(null, { status: 500 }) +} +``` + +### IDLE_TIMEOUT + +**Cause:** Your Edge Function did not respond within the [request timeout limit](/docs/guides/functions/limits). + +**Common causes:** + +- Long-running database queries +- Slow external API calls +- Infinite loops or blocking operations + +**Solution:** + +- Optimize slow operations +- Add timeout handling to external requests +- Consider breaking large operations into smaller chunks + +### WORKER_RESOURCE_LIMIT, WORKER_LIMIT + +**Cause:** Your Edge Function execution was stopped due to exceeding resource limits. Edge Function logs should indicate which [resource limit](/docs/guides/functions/limits) was exceeded. + +**Common causes:** + +- Memory usage exceeded available limits +- CPU time exceeded execution quotas +- Too many concurrent operations + +**Solution:** Check your Edge Function logs to see which resource limit was exceeded, then optimize your function accordingly. + +### WORKER_ERROR + +**Cause:** Your Edge Function threw an uncaught exception. + +```ts +// ... + +function initSomething() { + throw new Error('Some unhandled error') +} + +initSomething() // Error threw outside request handler + +export default { + fetch: withSupabase({ auth: 'none' }, async () => { + return new Response() + }), +} +``` + +**Common causes:** + +- Unhandled JavaScript errors in your function code, outside request handler +- Missing error handling for async operations +- Invalid JSON parsing + +**Solution:** Check your Edge Function logs to identify the specific error and add proper error handling to your code. + +### INVALID_RESPONSE_STATUS_CODE + +**Cause:** Your Edge Function is returning an invalid HTTP status code — not equal to `101` and outside the range `[200, 599]` + +**Common causes:** + +- Proxying an external service that returns an invalid HTTP status code + +```ts +// ... + +export default { + fetch: withSupabase({ auth: 'none' }, async (req) => { + // Fails in case this proxied server return a status >599 + return fetch('https://some-server-to-proxy', { + method: req.method, + headers: req.headers, + body: req.body, + }) + }), +} +``` + +**Solution:** + +- Ensure you are returning a valid HTTP status code +- For proxy endpoints, do not return the `fetch()` result directly; instead return a new `Response` wrapped in a try-catch block + +```ts +// ... + +export default { + fetch: withSupabase({ auth: 'none' }, async (req) => { + try { + const res = await fetch('https://some-server-to-proxy', { + method: req.method, + headers: req.headers, + body: req.body, + }) + + // Creating a 'new Response()' ensures contructor checks + return new Response(await res.body, { + headers: res.headers, + status: res.status, + statusText: res.statusText, + }) + } catch (e) { + console.error('Proxy Error', e) + return new Response(null, { status: 502 }) + } + }), +} +``` + +## Authentication Errors + +These errors occur when the request contains a missing, malformed, or unsupported JWT token. Fixing them requires ensuring your requests include a valid authorization header, or disabling JWT verification for public endpoints. +For further information, see [Authorization headers](/docs/guides/functions/auth-headers) and [Securing Edge Functions](/guides/functions/auth). + +### UNAUTHORIZED_NO_AUTH_HEADER + +**Cause:** The Edge Function has JWT verification enabled, but the request is missing the `Authorization` or `apikey` header. + +**Solution:** + +- Ensure you are passing a valid JWT token in the `Authorization` header +- Check that you are sending an API key in the `apikey` header +- For webhooks or public endpoints, consider disabling JWT verification + +### UNAUTHORIZED_ASYMMETRIC_JWT + +**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header contains an invalid asymmetric `ES256 | RS256` token. + +**Solution:** + +- Ensure you are passing a valid user JWT token in the `Authorization` header +- Check that your token has not expired + +### UNAUTHORIZED_LEGACY_JWT + +**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header contains an invalid legacy `HS256` token. + +**Solution:** + +- Ensure you are passing a valid legacy JWT token in the `Authorization` header +- Check that your token has not expired +- Verify that the legacy JWT secret has not been revoked or disabled + +### UNAUTHORIZED_UNSUPPORTED_TOKEN_ALGORITHM + +**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header does not contain an `ES256 | RS256 | HS256` token. + +**Solution:** + +- Ensure you are passing a valid Supabase-issued JWT token in the `Authorization` header + +### UNAUTHORIZED_INVALID_JWT_FORMAT + +**Cause:** The Edge Function has JWT verification enabled, but the `Authorization` header does not follow the `Bearer ` format. + +**Solution:** + +- Check that you are passing `Bearer ` in the `Authorization` header +- Ensure you are sending an API key in the `apikey` header instead of `Authorization` +- For webhooks or public endpoints, consider disabling JWT verification + +## Request Errors + +These errors indicate issues with the request itself, which typically require changing how the function is called. + +### RATE_LIMIT_EXCEEDED + +**Cause:** The platform detected [recursive or nested function call](/docs/guides/functions/recursive-functions) behavior. + +**Common causes:** + +- Multiple function-to-function calls +- Recursive or circular calls + +**Solution:** + +- Use the suggested retry window in seconds from the error message before calling your function again +- Ensure you are not performing unnecessary individual calls; use batch operations where possible +- Delegate large workloads to queues instead of recursively calling other Edge Functions + +### INVALID_URL + +**Cause:** The platform rejected a malformed URL. + +**Solution:** + +- Ensure you are calling with a valid [formatted URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) + +--- + +## Server Errors + +These errors indicate issues with function loading, execution, or the underlying platform. + +### NOT_FOUND + +**Cause:** The Edge Function metadata or files were not found or are missing in the specific region. + +**Solution:** Try redeploying your function and wait a few minutes to make sure all regions have been updated. + +### BOOT_ERROR + +**Cause:** Your Edge Function failed to start. + +**Common causes:** + +- Syntax errors preventing the function from loading +- Import errors or missing dependencies +- Invalid function configuration + +**Solution:** Check your Edge Function logs and also verify that your function code can be executed locally with `supabase functions serve`. + +### LOAD_FUNCTION_ERROR + +**Cause:** The platform was unable to load your function metadata or files. + +**Solution:** + +- Try calling your function again after a short delay +- If the problem persists, contact support + +### LOAD_FUNCTION_METADATA_ERROR + +**Cause:** The platform could not fetch your function metadata, possibly due to external cache issues. + +**Solution:** + +- Wait a few minutes before calling your function again +- If the problem persists, contact support + +### LOAD_FUNCTION_INVALID_ENTRYPOINT_PATH_ERROR + +**Cause:** Your Edge Function metadata is broken or contains an invalid entrypoint. + +**Solution:** + +- Try redeploying your function +- If the problem persists, contact support diff --git a/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx b/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx index 99808e2f6b9..04c258cb300 100644 --- a/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx +++ b/apps/docs/content/guides/functions/examples/amazon-bedrock-image-generator.mdx @@ -46,91 +46,83 @@ And add the code to the `index.ts` file: ```ts index.ts // We need to mock the file system for the AWS SDK to work. import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts' - import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime' -import { createClient } from 'npm:@supabase/supabase-js' +import { withSupabase } from 'npm:@supabase/server@^1' import { decode } from 'npm:base64-arraybuffer' console.log('Hello from Amazon Bedrock!') -const SUPABASE_PUBLISHABLE_KEYS = JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!) +// Called with a publishable key on the `apikey` header. Deploy with `verify_jwt = false`. +export default { + fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { + prepareVirtualFile('./aws/config') + prepareVirtualFile('./aws/credentials') -Deno.serve(async (req) => { - prepareVirtualFile('./aws/config') - prepareVirtualFile('./aws/credentials') - - const client = new BedrockRuntimeClient({ - region: Deno.env.get('AWS_DEFAULT_REGION') ?? 'us-west-2', - credentials: { - accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '', - secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '', - sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '', - }, - }) - - const { prompt, seed } = await req.json() - console.log(prompt) - const input = { - contentType: 'application/json', - accept: '*/*', - modelId: 'amazon.titan-image-generator-v1', - body: JSON.stringify({ - taskType: 'TEXT_IMAGE', - textToImageParams: { text: prompt }, - imageGenerationConfig: { - numberOfImages: 1, - quality: 'standard', - cfgScale: 8.0, - height: 512, - width: 512, - seed: seed ?? 0, + const client = new BedrockRuntimeClient({ + region: Deno.env.get('AWS_DEFAULT_REGION') ?? 'us-west-2', + credentials: { + accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '', + secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '', + sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '', }, - }), - } + }) - const command = new InvokeModelCommand(input) - const response = await client.send(command) - console.log(response) - - if (response.$metadata.httpStatusCode === 200) { - const { body, $metadata } = response - - const textDecoder = new TextDecoder('utf-8') - const jsonString = textDecoder.decode(body.buffer) - const parsedData = JSON.parse(jsonString) - console.log(parsedData) - const image = parsedData.images[0] - - const supabaseClient = createClient( - // Supabase API URL - env var exported by default. - Deno.env.get('SUPABASE_URL')!, - // Using the default Supabase API PUB KEY. - // If you want to use a different api key, change 'default' to your preferred key name - SUPABASE_PUBLISHABLE_KEYS['default'] - ) - - const { data: upload, error: uploadError } = await supabaseClient.storage - .from('images') - .upload(`${$metadata.requestId ?? ''}.png`, decode(image), { - contentType: 'image/png', - cacheControl: '3600', - upsert: false, - }) - if (!upload) { - return Response.json(uploadError) + const { prompt, seed } = await req.json() + console.log(prompt) + const input = { + contentType: 'application/json', + accept: '*/*', + modelId: 'amazon.titan-image-generator-v1', + body: JSON.stringify({ + taskType: 'TEXT_IMAGE', + textToImageParams: { text: prompt }, + imageGenerationConfig: { + numberOfImages: 1, + quality: 'standard', + cfgScale: 8.0, + height: 512, + width: 512, + seed: seed ?? 0, + }, + }), } - const { data } = supabaseClient.storage.from('images').getPublicUrl(upload.path!) - return Response.json(data) - } - return Response.json(response) -}) + const command = new InvokeModelCommand(input) + const response = await client.send(command) + console.log(response) + + if (response.$metadata.httpStatusCode === 200) { + const { body, $metadata } = response + + const textDecoder = new TextDecoder('utf-8') + const jsonString = textDecoder.decode(body.buffer) + const parsedData = JSON.parse(jsonString) + console.log(parsedData) + const image = parsedData.images[0] + + const { data: upload, error: uploadError } = await ctx.supabase.storage + .from('images') + .upload(`${$metadata.requestId ?? ''}.png`, decode(image), { + contentType: 'image/png', + cacheControl: '3600', + upsert: false, + }) + if (!upload) { + return Response.json(uploadError) + } + const { data } = ctx.supabase.storage.from('images').getPublicUrl(upload.path!) + return Response.json(data) + } + + return Response.json(response) + }), +} ``` ## Run the function locally 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) -2. Start with env: `supabase functions serve --env-file supabase/.env` +2. Start with env: `supabase functions serve --no-verify-jwt --env-file supabase/.env` 3. Make an HTTP request: ```bash @@ -146,7 +138,7 @@ Deno.serve(async (req) => { ```bash supabase link -supabase functions deploy amazon-bedrock +supabase functions deploy amazon-bedrock --no-verify-jwt supabase secrets set --env-file supabase/.env ``` diff --git a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx index e21a4dfab24..7b10c3140c6 100644 --- a/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx +++ b/apps/docs/content/guides/functions/examples/auth-send-email-hook-react-email-resend.mdx @@ -34,84 +34,80 @@ supabase functions new send-email Paste the following code into the `index.ts` file: ```tsx supabase/functions/send-email/index.ts -import React from 'npm:react@18.3.1' import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' -import { Resend } from 'npm:resend@4.0.0' import { renderAsync } from 'npm:@react-email/components@0.0.22' +import { withSupabase } from 'npm:@supabase/server@^1' +import React from 'npm:react@18.3.1' +import { Resend } from 'npm:resend@4.0.0' + import { MagicLinkEmail } from './_templates/magic-link.tsx' const resend = new Resend(Deno.env.get('RESEND_API_KEY') as string) const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v1,whsec_', '') -Deno.serve(async (req) => { - if (req.method !== 'POST') { - return new Response('not allowed', { status: 400 }) - } - - const payload = await req.text() - const headers = Object.fromEntries(req.headers) - const wh = new Webhook(hookSecret) - try { - const { - user, - email_data: { token, token_hash, redirect_to, email_action_type }, - } = wh.verify(payload, headers) as { - user: { - email: string - } - email_data: { - token: string - token_hash: string - redirect_to: string - email_action_type: string - site_url: string - token_new: string - token_hash_new: string - } +export default { + fetch: withSupabase({ auth: 'none' }, async (req) => { + if (req.method !== 'POST') { + return new Response('not allowed', { status: 400 }) } - const html = await renderAsync( - React.createElement(MagicLinkEmail, { - supabase_url: Deno.env.get('SUPABASE_URL') ?? '', - token, - token_hash, - redirect_to, - email_action_type, + const payload = await req.text() + const headers = Object.fromEntries(req.headers) + const wh = new Webhook(hookSecret) + try { + const { + user, + email_data: { token, token_hash, redirect_to, email_action_type }, + } = wh.verify(payload, headers) as { + user: { + email: string + } + email_data: { + token: string + token_hash: string + redirect_to: string + email_action_type: string + site_url: string + token_new: string + token_hash_new: string + } + } + + const html = await renderAsync( + React.createElement(MagicLinkEmail, { + supabase_url: Deno.env.get('SUPABASE_URL') ?? '', + token, + token_hash, + redirect_to, + email_action_type, + }) + ) + + const { error } = await resend.emails.send({ + from: 'welcome ', + to: [user.email], + subject: 'Supa Custom MagicLink!', + html, }) - ) - - const { error } = await resend.emails.send({ - from: 'welcome ', - to: [user.email], - subject: 'Supa Custom MagicLink!', - html, - }) - if (error) { - throw error - } - } catch (error) { - console.log(error) - return new Response( - JSON.stringify({ - error: { - http_code: error.code, - message: error.message, - }, - }), - { - status: 401, - headers: { 'Content-Type': 'application/json' }, + if (error) { + throw error } - ) - } + } catch (error) { + console.log(error) + return Response.json( + { + error: { + http_code: error.code, + message: error.message, + }, + }, + { status: 401 } + ) + } - const responseHeaders = new Headers() - responseHeaders.set('Content-Type', 'application/json') - return new Response(JSON.stringify({}), { - status: 200, - headers: responseHeaders, - }) -}) + return Response.json({}) + }), +} ``` ### 3. Create React Email templates diff --git a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx index 8d6c208e3c3..f28412b1025 100644 --- a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx +++ b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx @@ -23,7 +23,7 @@ supabase functions new cloudflare-turnstile And add the code to the `index.ts` file: ```ts index.ts -import { corsHeaders } from '@supabase/supabase-js/cors' // v2.95.0+ +import { withSupabase } from 'npm:@supabase/server@^1' console.log('Hello from Cloudflare Trunstile!') @@ -31,36 +31,34 @@ function ips(req: Request) { return req.headers.get('x-forwarded-for')?.split(/\s*,\s*/) } -Deno.serve(async (req) => { - // This is needed if you're planning to invoke your function from a browser. - if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }) - } +// `withSupabase` handles CORS and preflight requests for you. +export default { + fetch: withSupabase({ auth: 'none' }, async (req) => { + const { token } = await req.json() + const clientIps = ips(req) || [''] + const ip = clientIps[0] - const { token } = await req.json() - const clientIps = ips(req) || [''] - const ip = clientIps[0] + // Validate the token by calling the + // "/siteverify" API endpoint. + let formData = new FormData() + formData.append('secret', Deno.env.get('CLOUDFLARE_SECRET_KEY') ?? '') + formData.append('response', token) + formData.append('remoteip', ip) - // Validate the token by calling the - // "/siteverify" API endpoint. - let formData = new FormData() - formData.append('secret', Deno.env.get('CLOUDFLARE_SECRET_KEY') ?? '') - formData.append('response', token) - formData.append('remoteip', ip) + const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' + const result = await fetch(url, { + body: formData, + method: 'POST', + }) - const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' - const result = await fetch(url, { - body: formData, - method: 'POST', - }) - - const outcome = await result.json() - console.log(outcome) - if (outcome.success) { - return new Response('success', { headers: corsHeaders }) - } - return new Response('failure', { headers: corsHeaders }) -}) + const outcome = await result.json() + console.log(outcome) + if (outcome.success) { + return new Response('success') + } + return new Response('failure') + }), +} ``` ## Deploy the server-side validation Edge Functions @@ -68,7 +66,7 @@ Deno.serve(async (req) => { - https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ ```bash -supabase functions deploy cloudflare-turnstile +supabase functions deploy cloudflare-turnstile --no-verify-jwt supabase secrets set CLOUDFLARE_SECRET_KEY=your_secret_key ``` diff --git a/apps/docs/content/guides/functions/examples/discord-bot.mdx b/apps/docs/content/guides/functions/examples/discord-bot.mdx index aa00105e557..a7ba70a4806 100644 --- a/apps/docs/content/guides/functions/examples/discord-bot.mdx +++ b/apps/docs/content/guides/functions/examples/discord-bot.mdx @@ -43,12 +43,13 @@ This will register a Slash Command named `hello` that accepts a parameter named ```ts index.ts // Sift is a small routing library that abstracts away details like starting a -// listener on a port, and provides a simple function (serve) that has an API +// listener on a port, and provides a function (serve) that has an API // to invoke a function for a specific path. -import { json, serve, validateRequest } from 'https://deno.land/x/sift@0.6.0/mod.ts' + // TweetNaCl is a cryptography library that we use to verify requests // from Discord. import nacl from 'https://cdn.skypack.dev/tweetnacl@v1.0.3?dts' +import { json, serve, validateRequest } from 'https://deno.land/x/sift@0.6.0/mod.ts' enum DiscordCommandType { Ping = 1, @@ -152,7 +153,7 @@ Navigate to your Function details in the Supabase Dashboard to get your Endpoint 1. Go back to your application (Greeter) page on Discord Developer Portal 2. Fill **INTERACTIONS ENDPOINT URL** field with the URL and click on **Save Changes**. -The application is now ready. Let's proceed to the next section to install it. +The application is now ready. Proceed to the next section to install it. ## Install the slash command on your Discord server diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx index 6ea7ac92d9c..d6ef95ec477 100644 --- a/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx +++ b/apps/docs/content/guides/functions/examples/elevenlabs-generate-speech-stream.mdx @@ -100,95 +100,91 @@ In your newly created `supabase/functions/text-to-speech/index.ts` file, add the ```ts supabase/functions/text-to-speech/index.ts // Setup type definitions for built-in Supabase Runtime APIs import 'jsr:@supabase/functions-js/edge-runtime.d.ts' -import { createClient } from 'npm:@supabase/supabase-js@2' + +import { withSupabase } from 'npm:@supabase/server@^1' import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0' import * as hash from 'npm:object-hash' -const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) - -// If you want to use a different api key, change 'default' to your preferred key name -const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']) - const client = new ElevenLabsClient({ apiKey: Deno.env.get('ELEVENLABS_API_KEY'), }) -// Upload audio to Supabase Storage in a background task -async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) { - const { data, error } = await supabase.storage - .from('audio') - .upload(`${requestHash}.mp3`, stream, { - contentType: 'audio/mp3', - }) +// Deploy with verify_jwt = false +// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode. +export default { + fetch: withSupabase({ auth: 'none' }, async (req, ctx) => { + // Upload audio to Supabase Storage in a background task + async function uploadAudioToStorage(stream: ReadableStream, requestHash: string) { + const { data, error } = await ctx.supabaseAdmin.storage + .from('audio') + .upload(`${requestHash}.mp3`, stream, { + contentType: 'audio/mp3', + }) - console.log('Storage upload result', { data, error }) + console.log('Storage upload result', { data, error }) + } + + // To secure your function for production, you can for example validate the request origin, + // or append a user access token and validate it with Supabase Auth. + console.log('Request origin', req.headers.get('host')) + const url = new URL(req.url) + const params = new URLSearchParams(url.search) + const text = params.get('text') + const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb' + + const requestHash = hash.MD5({ text, voiceId }) + console.log('Request hash', requestHash) + + // Check storage for existing audio file + const { data } = await ctx.supabaseAdmin.storage + .from('audio') + .createSignedUrl(`${requestHash}.mp3`, 60) + + if (data) { + console.log('Audio file found in storage', data) + const storageRes = await fetch(data.signedUrl) + if (storageRes.ok) return storageRes + } + + if (!text) { + return Response.json({ error: 'Text parameter is required' }, { status: 400 }) + } + + try { + console.log('ElevenLabs API call') + const response = await client.textToSpeech.convertAsStream(voiceId, { + output_format: 'mp3_44100_128', + model_id: 'eleven_multilingual_v2', + text, + }) + + const stream = new ReadableStream({ + async start(controller) { + for await (const chunk of response) { + controller.enqueue(chunk) + } + controller.close() + }, + }) + + // Branch stream to Supabase Storage + const [browserStream, storageStream] = stream.tee() + + // Upload to Supabase Storage in the background + EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash)) + + // Return the streaming response immediately + return new Response(browserStream, { + headers: { + 'Content-Type': 'audio/mpeg', + }, + }) + } catch (error) { + console.log('error', { error }) + return Response.json({ error: error.message }, { status: 500 }) + } + }), } - -Deno.serve(async (req) => { - // To secure your function for production, you can for example validate the request origin, - // or append a user access token and validate it with Supabase Auth. - console.log('Request origin', req.headers.get('host')) - const url = new URL(req.url) - const params = new URLSearchParams(url.search) - const text = params.get('text') - const voiceId = params.get('voiceId') ?? 'JBFqnCBsd6RMkjVDRZzb' - - const requestHash = hash.MD5({ text, voiceId }) - console.log('Request hash', requestHash) - - // Check storage for existing audio file - const { data } = await supabase.storage.from('audio').createSignedUrl(`${requestHash}.mp3`, 60) - - if (data) { - console.log('Audio file found in storage', data) - const storageRes = await fetch(data.signedUrl) - if (storageRes.ok) return storageRes - } - - if (!text) { - return new Response(JSON.stringify({ error: 'Text parameter is required' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }) - } - - try { - console.log('ElevenLabs API call') - const response = await client.textToSpeech.convertAsStream(voiceId, { - output_format: 'mp3_44100_128', - model_id: 'eleven_multilingual_v2', - text, - }) - - const stream = new ReadableStream({ - async start(controller) { - for await (const chunk of response) { - controller.enqueue(chunk) - } - controller.close() - }, - }) - - // Branch stream to Supabase Storage - const [browserStream, storageStream] = stream.tee() - - // Upload to Supabase Storage in the background - EdgeRuntime.waitUntil(uploadAudioToStorage(storageStream, requestHash)) - - // Return the streaming response immediately - return new Response(browserStream, { - headers: { - 'Content-Type': 'audio/mpeg', - }, - }) - } catch (error) { - console.log('error', { error }) - return new Response(JSON.stringify({ error: error.message }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } -}) ``` ## Run locally diff --git a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx index db8248876c3..e4913a30afa 100644 --- a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx +++ b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx @@ -108,8 +108,11 @@ In your newly created `scribe-bot/index.ts` file, add the following code: ```ts supabase/functions/scribe-bot/index.ts import { Bot, webhookCallback } from 'https://deno.land/x/grammy@v1.34.0/mod.ts' + import 'jsr:@supabase/functions-js/edge-runtime.d.ts' -import { createClient } from 'npm:@supabase/supabase-js@2' + +import { withSupabase } from 'npm:@supabase/server@^1' +import type { SupabaseClient } from 'npm:@supabase/supabase-js@2' import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5' console.log(`Function "elevenlabs-scribe-bot" up and running!`) @@ -117,13 +120,9 @@ console.log(`Function "elevenlabs-scribe-bot" up and running!`) const elevenLabsClient = new ElevenLabsClient({ apiKey: Deno.env.get('ELEVENLABS_API_KEY') || '', }) -const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) -const supabase = createClient( - Deno.env.get('SUPABASE_URL') || '', - Deno.env.get(SUPABASE_SECRET_KEYS['default']) || '' -) async function scribe({ + supabaseAdmin, fileURL, fileType, duration, @@ -131,6 +130,7 @@ async function scribe({ messageId, username, }: { + supabaseAdmin: SupabaseClient fileURL: string fileType: string duration: number @@ -178,9 +178,13 @@ async function scribe({ error: errorMsg, } console.log({ logLine }) - await supabase.from('transcription_logs').insert({ ...logLine, transcript }) + await supabaseAdmin.from('transcription_logs').insert({ ...logLine, transcript }) } +// Set by the request handler before delegating to grammY, so bot handlers +// can write transcription logs with the admin client. +let supabaseAdmin: SupabaseClient + const telegramBotToken = Deno.env.get('TELEGRAM_BOT_TOKEN') const bot = new Bot(telegramBotToken || '') const startMessage = `Welcome to the ElevenLabs Scribe Bot\\! I can transcribe speech in 99 languages with super high accuracy\\! @@ -202,6 +206,7 @@ bot.on([':voice', ':audio', ':video'], async (ctx) => { // Run the transcription in the background. EdgeRuntime.waitUntil( scribe({ + supabaseAdmin, fileURL, fileType: fileMeta.mime_type!, duration: fileMeta.duration, @@ -223,18 +228,24 @@ bot.on([':voice', ':audio', ':video'], async (ctx) => { const handleUpdate = webhookCallback(bot, 'std/http') -Deno.serve(async (req) => { - try { - const url = new URL(req.url) - if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) { - return new Response('not allowed', { status: 405 }) - } +// Deploy with verify_jwt = false +// The bot is called by Telegram, so we verify the request with FUNCTION_SECRET in code. +export default { + fetch: withSupabase({ auth: 'none' }, async (req, ctx) => { + try { + const url = new URL(req.url) + if (url.searchParams.get('secret') !== Deno.env.get('FUNCTION_SECRET')) { + return new Response('not allowed', { status: 405 }) + } - return await handleUpdate(req) - } catch (err) { - console.error(err) - } -}) + supabaseAdmin = ctx.supabaseAdmin + + return await handleUpdate(req) + } catch (err) { + console.error(err) + } + }), +} ``` ## Deploy to Supabase diff --git a/apps/docs/content/guides/functions/examples/image-manipulation.mdx b/apps/docs/content/guides/functions/examples/image-manipulation.mdx index 3afed37cb90..7bddc3ed424 100644 --- a/apps/docs/content/guides/functions/examples/image-manipulation.mdx +++ b/apps/docs/content/guides/functions/examples/image-manipulation.mdx @@ -61,7 +61,7 @@ If you open the `output.png` file you will find a transformed version of your or ### Deploy to your hosted project -Now, let's deploy the function to your Supabase project. +Deploy the function to your Supabase project. ```bash supabase link diff --git a/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx b/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx index f81fe9b86f6..fc22b0f717a 100644 --- a/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx +++ b/apps/docs/content/guides/functions/examples/mcp-server-mcp-lite.mdx @@ -21,7 +21,7 @@ This combination offers several advantages: - **Direct database access**: Connect directly to your Supabase Postgres - **Minimal footprint**: mcp-lite has zero runtime dependencies - **Full type safety**: TypeScript support in Deno -- **Simple deployment**: One command to production +- **Basic deployment**: One command to production ## Prerequisites diff --git a/apps/docs/content/guides/functions/examples/og-image.mdx b/apps/docs/content/guides/functions/examples/og-image.mdx index ec076c16b7c..71cef4ad1ee 100644 --- a/apps/docs/content/guides/functions/examples/og-image.mdx +++ b/apps/docs/content/guides/functions/examples/og-image.mdx @@ -21,8 +21,8 @@ Generate Open Graph images with Deno and Supabase Edge Functions. [View on GitHu Create a `handler.tsx` file to construct the OG image in React: ```tsx handler.tsx -import React from 'https://esm.sh/react@18.2.0' import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts' +import React from 'https://esm.sh/react@18.2.0' export default function handler(req: Request) { return new ImageResponse( @@ -46,9 +46,12 @@ export default function handler(req: Request) { Create an `index.ts` file to execute the handler on incoming requests: ```ts index.ts +import { withSupabase } from 'npm:@supabase/server@^1' + import handler from './handler.tsx' console.log('Hello from og-image Function!') -Deno.serve(handler) +// Public image endpoint, so deploy with --no-verify-jwt. +export default { fetch: withSupabase({ auth: 'none' }, handler) } ``` diff --git a/apps/docs/content/guides/functions/examples/push-notifications.mdx b/apps/docs/content/guides/functions/examples/push-notifications.mdx index d36d99ef201..dc9b26e33d3 100644 --- a/apps/docs/content/guides/functions/examples/push-notifications.mdx +++ b/apps/docs/content/guides/functions/examples/push-notifications.mdx @@ -27,7 +27,7 @@ Push notifications are an important part of any mobile app. They allow you to se ## Expo setup - To utilize Expo's push notification service, you must configure your app by installing a set of libraries, implementing functions to handle notifications, and setting up credentials for Android and iOS. Follow the official [Expo Push Notifications Setup Guide](https://docs.expo.dev/push-notifications/push-notifications-setup/) to get the credentials for Android and iOS. This project uses [Expo's EAS build](https://docs.expo.dev/build/introduction/) service to simplify this part. + To use Expo's push notification service, you must configure your app by installing a set of libraries, implementing functions to handle notifications, and setting up credentials for Android and iOS. Follow the official [Expo Push Notifications Setup Guide](https://docs.expo.dev/push-notifications/push-notifications-setup/) to get the credentials for Android and iOS. This project uses [Expo's EAS build](https://docs.expo.dev/build/introduction/) service to simplify this part. 1. Install the dependencies: `npm i` 1. Create a [new Expo project](https://expo.dev/accounts/_/projects) @@ -53,7 +53,7 @@ Push notifications are an important part of any mobile app. They allow you to se 1. `supabase secrets set --env-file .env.local` ```ts supabase/functions/push/index.ts - import { createClient } from 'npm:@supabase/supabase-js@2' + import { withSupabase } from 'npm:@supabase/server@^1' console.log('Hello from Functions!') @@ -71,39 +71,33 @@ Push notifications are an important part of any mobile app. They allow you to se old_record: null | Notification } - const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) + // Triggered by a Database Webhook, which authenticates with a secret key. + // Deploy with `verify_jwt = false`. + export default { + fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => { + const payload: WebhookPayload = await req.json() + const { data } = await ctx.supabaseAdmin + .from('profiles') + .select('expo_push_token') + .eq('id', payload.record.user_id) + .single() - // If you want to use a different api key, change 'default' to your preferred key name - const supabase = createClient( - Deno.env.get('SUPABASE_URL')!, - SUPABASE_SECRET_KEYS['default'] - ) + const res = await fetch('https://exp.host/--/api/v2/push/send', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`, + }, + body: JSON.stringify({ + to: data?.expo_push_token, + sound: 'default', + body: payload.record.body, + }), + }).then((res) => res.json()) - Deno.serve(async (req) => { - const payload: WebhookPayload = await req.json() - const { data } = await supabase - .from('profiles') - .select('expo_push_token') - .eq('id', payload.record.user_id) - .single() - - const res = await fetch('https://exp.host/--/api/v2/push/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${Deno.env.get('EXPO_ACCESS_TOKEN')}`, - }, - body: JSON.stringify({ - to: data?.expo_push_token, - sound: 'default', - body: payload.record.body, - }), - }).then((res) => res.json()) - - return new Response(JSON.stringify(res), { - headers: { 'Content-Type': 'application/json' }, - }) - }) + return Response.json(res) + }), + } ``` ## Create the database webhook @@ -169,7 +163,7 @@ Push notifications are an important part of any mobile app. They allow you to se Add the following code to `supabase/functions/push/index.ts`: ```ts supabase/functions/push/index.ts - import { createClient } from 'npm:@supabase/supabase-js@2' + import { withSupabase } from 'npm:@supabase/server@^1' import { JWT } from 'npm:google-auth-library@9' import serviceAccount from '../service-account.json' with { type: 'json' } @@ -186,59 +180,53 @@ Push notifications are an important part of any mobile app. They allow you to se schema: 'public' } - const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) + // Triggered by a Database Webhook, which authenticates with a secret key. + // Deploy with `verify_jwt = false`. + export default { + fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => { + const payload: WebhookPayload = await req.json() - // If you want to use a different api key, change 'default' to your preferred key name - const supabase = createClient( - Deno.env.get('SUPABASE_URL')!, - SUPABASE_SECRET_KEYS['default'] - ) + const { data } = await ctx.supabaseAdmin + .from('profiles') + .select('fcm_token') + .eq('id', payload.record.user_id) + .single() - Deno.serve(async (req) => { - const payload: WebhookPayload = await req.json() + const fcmToken = data!.fcm_token as string - const { data } = await supabase - .from('profiles') - .select('fcm_token') - .eq('id', payload.record.user_id) - .single() + const accessToken = await getAccessToken({ + clientEmail: serviceAccount.client_email, + privateKey: serviceAccount.private_key, + }) - const fcmToken = data!.fcm_token as string - - const accessToken = await getAccessToken({ - clientEmail: serviceAccount.client_email, - privateKey: serviceAccount.private_key, - }) - - const res = await fetch( - `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - message: { - token: fcmToken, - notification: { - title: `Notification from Supabase`, - body: payload.record.body, - }, + const res = await fetch( + `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, }, - }), + body: JSON.stringify({ + message: { + token: fcmToken, + notification: { + title: `Notification from Supabase`, + body: payload.record.body, + }, + }, + }), + } + ) + + const resData = await res.json() + if (res.status < 200 || 299 < res.status) { + throw resData } - ) - const resData = await res.json() - if (res.status < 200 || 299 < res.status) { - throw resData - } - - return new Response(JSON.stringify(resData), { - headers: { 'Content-Type': 'application/json' }, - }) - }) + return Response.json(resData) + }), + } const getAccessToken = ({ clientEmail, diff --git a/apps/docs/content/guides/functions/examples/resumable-websockets.mdx b/apps/docs/content/guides/functions/examples/resumable-websockets.mdx new file mode 100644 index 00000000000..0c63efdb2f0 --- /dev/null +++ b/apps/docs/content/guides/functions/examples/resumable-websockets.mdx @@ -0,0 +1,225 @@ +--- +title: 'Resumable WebSockets with Edge Functions' +description: 'Build reconnect-safe WebSockets with event replay, idempotency keys, and graceful restarts.' +--- + +This example shows how to build a reconnect-safe chat stream on Supabase Edge Functions using: + +- WebSocket upgrade + JWT auth +- Postgres-backed session and event persistence +- Event replay with `lastEventId` +- Idempotent user messages with `idempotency_key` +- Graceful client reconnects during worker restarts + +Reference implementation: [Building Resumable WebSockets with Supabase Edge Functions and Postgres](https://blog.mansueli.com/building-resumable-websockets-with-supabase-edge-functions-and-postgres) + +## Architecture + +1. Client connects with a user JWT, plus optional `sessionId` and `lastEventId`. +2. Function verifies auth and either resumes or creates a session. +3. Every message is written to `ws_events` with an incrementing `id`. +4. On reconnect, server replays events where `id > lastEventId`. +5. Client updates local `lastEventId` and resumes without losing messages. + +## Database schema + +```sql +create extension if not exists pgcrypto; + +create table ws_sessions ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null, + created_at timestamptz default now(), + updated_at timestamptz default now(), + last_event_id bigint default 0 +); + +create table ws_events ( + id bigint generated by default as identity primary key, + session_id uuid not null references ws_sessions(id) on delete cascade, + event_type text not null, + payload jsonb not null, + created_at timestamptz default now() +); +create index ws_events_session_id_id_idx on ws_events(session_id, id); + +create table ws_idempotency_keys ( + session_id uuid not null references ws_sessions(id) on delete cascade, + idempotency_key uuid not null, + primary key(session_id, idempotency_key) +); + +create unlogged table ws_live_connections ( + session_id uuid primary key, + connected_at timestamptz default now(), + last_seen_at timestamptz default now(), + edge_region text +); +``` + +## Edge Function (WebSocket proxy) + +Use `supabase functions serve --no-verify-jwt` and validate JWT inside the function. + +```ts +import { createAdminClient, createContextClient, verifyCredentials } from '@supabase/server/core' + +const PREEMPTIVE_RESTART_MS = 340_000 + +function send(socket: WebSocket, payload: unknown) { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(payload)) + } +} + +Deno.serve(async (req) => { + const url = new URL(req.url) + const token = url.searchParams.get('token') + if (!token) return new Response('Missing token', { status: 401 }) + + const { data: auth, error } = await verifyCredentials({ token, apikey: null }, { auth: 'user' }) + if (error || !auth?.userClaims?.id) { + return new Response('Unauthorized', { status: 401 }) + } + + const admin = createAdminClient() + const { socket, response } = Deno.upgradeWebSocket(req, { idleTimeout: 0 }) + + // Prevent EarlyDrop by keeping a pending promise until socket close. + let resolveClosed!: () => void + const closed = new Promise((resolve) => { + resolveClosed = resolve + }) + // @ts-ignore + EdgeRuntime.waitUntil(closed) + + const requestedSessionId = url.searchParams.get('sessionId') + const lastEventId = Number(url.searchParams.get('lastEventId') || 0) + const sessionId = requestedSessionId ?? crypto.randomUUID() + + socket.onclose = () => { + resolveClosed() + } + + socket.onmessage = async (event) => { + const msg = JSON.parse(event.data) + + if (msg.type === 'user_message') { + const { error: idempotencyError } = await admin.from('ws_idempotency_keys').upsert( + { + session_id: sessionId, + idempotency_key: msg.idempotency_key, + }, + { onConflict: 'session_id,idempotency_key', ignoreDuplicates: true } + ) + + let userEvent + + if (idempotencyError) { + // Conflict detected - this is a retry, fetch the existing event + const { data: existingEvent } = await admin + .from('ws_events') + .select() + .eq('session_id', sessionId) + .eq('idempotency_key', msg.idempotency_key) + .single() + + userEvent = existingEvent + } else { + // New idempotency key - insert the event + const { data: newEvent } = await admin + .from('ws_events') + .insert({ + session_id: sessionId, + event_type: 'user_message', + payload: { content: msg.content }, + }) + .select() + .single() + + userEvent = newEvent + } + + send(socket, { + type: 'user_message', + payload: userEvent?.payload, + event_id: userEvent?.id, + }) + } + } + + send(socket, { type: 'session_init', session_id: sessionId }) + + queueMicrotask(async () => { + const { data: replayEvents } = await admin + .from('ws_events') + .select('*') + .eq('session_id', sessionId) + .gt('id', lastEventId) + .order('id') + + for (const event of replayEvents ?? []) { + send(socket, { + type: event.event_type, + payload: event.payload, + event_id: event.id, + replay: true, + }) + } + }) + + setTimeout(() => { + send(socket, { type: 'server_restarting' }) + socket.close(1012, 'Service restart') + }, PREEMPTIVE_RESTART_MS) + + return response +}) +``` + +## Browser client + +The client stores `sessionId` and `lastEventId` in session storage, then reconnects with exponential backoff. + +```ts +let sessionId = sessionStorage.getItem('ws_session_id') +let lastEventId = Number(sessionStorage.getItem('last_event_id') || 0) + +function connect(token: string) { + const url = + `wss://YOUR_PROJECT.functions.supabase.co/websocket-proxy` + + `?token=${encodeURIComponent(token)}` + + `&lastEventId=${lastEventId}` + + (sessionId ? `&sessionId=${sessionId}` : '') + + const ws = new WebSocket(url) + + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) + + if (msg.event_id) { + lastEventId = Math.max(lastEventId, msg.event_id) + sessionStorage.setItem('last_event_id', String(lastEventId)) + } + + if (msg.type === 'session_init') { + sessionId = msg.session_id + sessionStorage.setItem('ws_session_id', sessionId) + } + } +} +``` + +## Why this pattern works + +- If the worker restarts, the client reconnects with the same session. +- Replay closes delivery gaps caused by reconnect windows. +- Idempotency keys prevent duplicate inserts when clients retry. +- `EdgeRuntime.waitUntil()` prevents unexpected early termination of idle-looking WebSocket workers. + +## Next steps + +- Add row-level security policies for all `ws_*` tables. +- Add a heartbeat and cleanup policy for stale sessions. +- Add structured event payload types and input validation. +- Add observability dashboards for disconnect rate and replay lag. diff --git a/apps/docs/content/guides/functions/examples/semantic-search.mdx b/apps/docs/content/guides/functions/examples/semantic-search.mdx index db717653b6c..78d803f4b1d 100644 --- a/apps/docs/content/guides/functions/examples/semantic-search.mdx +++ b/apps/docs/content/guides/functions/examples/semantic-search.mdx @@ -37,33 +37,39 @@ create index on embeddings using hnsw (embedding vector_ip_ops); You can deploy the [following edge function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) as a [database webhook](/docs/guides/database/webhooks) to generate the embeddings for any text content inserted into the table: -```tsx +```ts +import { withSupabase } from 'npm:@supabase/server@^1' + const model = new Supabase.ai.Session('gte-small') -Deno.serve(async (req) => { - const payload: WebhookPayload = await req.json() - const { content, id } = payload.record +// Triggered by a Database Webhook, which authenticates with a secret key. +// Deploy with `verify_jwt = false`. +export default { + fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => { + const payload: WebhookPayload = await req.json() + const { content, id } = payload.record - // Generate embedding. - const embedding = await model.run(content, { - mean_pool: true, - normalize: true, - }) + // Generate embedding. + const embedding = await model.run(content, { + mean_pool: true, + normalize: true, + }) - // Store in database. - const { error } = await supabase - .from('embeddings') - .update({ embedding: JSON.stringify(embedding) }) - .eq('id', id) - if (error) console.warn(error.message) + // Store in database. + const { error } = await ctx.supabaseAdmin + .from('embeddings') + .update({ embedding: JSON.stringify(embedding) }) + .eq('id', id) + if (error) console.warn(error.message) - return new Response('ok') -}) + return new Response('ok') + }), +} ``` ## Create a Database Function and RPC -With the embeddings now stored in your Postgres database table, you can query them from Supabase Edge Functions by utilizing [Remote Procedure Calls (RPC)](/docs/guides/database/functions?language=js). +With the embeddings now stored in your Postgres database table, you can query them from Supabase Edge Functions by using [Remote Procedure Calls (RPC)](/docs/guides/database/functions?language=js). Given the [following Postgres Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql): @@ -98,32 +104,36 @@ $$; You can use `supabase-js` to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your [Supabase Edge Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/search/index.ts): -```tsx +```ts +import { withSupabase } from 'npm:@supabase/server@^1' + const model = new Supabase.ai.Session('gte-small') -Deno.serve(async (req) => { - const { search } = await req.json() - if (!search) return new Response('Please provide a search param!') - // Generate embedding for search term. - const embedding = await model.run(search, { - mean_pool: true, - normalize: true, - }) - - // Query embeddings. - const { data: result, error } = await supabase - .rpc('query_embeddings', { - embedding, - match_threshold: 0.8, +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + const { search } = await req.json() + if (!search) return new Response('Please provide a search param!') + // Generate embedding for search term. + const embedding = await model.run(search, { + mean_pool: true, + normalize: true, }) - .select('content') - .limit(3) - if (error) { - return Response.json(error) - } - return Response.json({ search, result }) -}) + // Query embeddings. + const { data: result, error } = await ctx.supabase + .rpc('query_embeddings', { + embedding, + match_threshold: 0.8, + }) + .select('content') + .limit(3) + if (error) { + return Response.json(error) + } + + return Response.json({ search, result }) + }), +} ``` You now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions! diff --git a/apps/docs/content/guides/functions/examples/send-emails.mdx b/apps/docs/content/guides/functions/examples/send-emails.mdx index 60fc7926ad5..c9bcef0f723 100644 --- a/apps/docs/content/guides/functions/examples/send-emails.mdx +++ b/apps/docs/content/guides/functions/examples/send-emails.mdx @@ -30,6 +30,8 @@ Store the `RESEND_API_KEY` in your `.env` file. Paste the following code into the `index.ts` file: ```tsx +import { withSupabase } from 'npm:@supabase/server@^1' + const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY') const handler = async (_request: Request): Promise => { @@ -49,15 +51,10 @@ const handler = async (_request: Request): Promise => { const data = await res.json() - return new Response(JSON.stringify(data), { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }) + return Response.json(data) } -Deno.serve(handler) +export default { fetch: withSupabase({ auth: ['user', 'secret'] }, handler) } ``` ### 3. Deploy and send email @@ -69,7 +66,12 @@ supabase start supabase functions serve --no-verify-jwt --env-file .env ``` -Test it: http://localhost:54321/functions/v1/resend +The function accepts a signed-in user's JWT or a secret key, so it can be triggered from your app (via `supabase.functions.invoke`) or from a database function. Test it locally with a secret key: + +```bash +curl -i --request POST 'http://localhost:54321/functions/v1/resend' \ + --header 'apikey: ' +``` Deploy function to Supabase: @@ -83,8 +85,6 @@ When you deploy to Supabase, make sure that your `RESEND_API_KEY` is set in [Edg -Open the endpoint URL to send an email: - ### 4. Try it yourself Find the complete example on [GitHub](https://github.com/resendlabs/resend-supabase-edge-functions-example). diff --git a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx index ae6d901ebae..9a691e28233 100644 --- a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx +++ b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx @@ -24,6 +24,7 @@ Handle exceptions within your function and send them to Sentry. ```tsx import * as Sentry from 'https://deno.land/x/sentry/index.mjs' +import { withSupabase } from 'npm:@supabase/server@^1' Sentry.init({ // https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn @@ -39,25 +40,25 @@ Sentry.init({ Sentry.setTag('region', Deno.env.get('SB_REGION')) Sentry.setTag('execution_id', Deno.env.get('SB_EXECUTION_ID')) -Deno.serve(async (req) => { - try { - const { name } = await req.json() - // This will throw, as `name` in our example call will be `undefined` - const data = { - message: `Hello ${name}!`, - } +// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode. +export default { + fetch: withSupabase({ auth: 'none' }, async (req, ctx) => { + try { + const { name } = await req.json() + // This will throw, as `name` in our example call will be `undefined` + const data = { + message: `Hello ${name}!`, + } - return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }) - } catch (e) { - Sentry.captureException(e) - // Flush Sentry before the running process closes - await Sentry.flush(2000) - return new Response(JSON.stringify({ msg: 'error' }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } -}) + return Response.json(data) + } catch (e) { + Sentry.captureException(e) + // Flush Sentry before the running process closes + await Sentry.flush(2000) + return Response.json({ msg: 'error' }, { status: 500 }) + } + }), +} ``` ### 3. Deploy and test diff --git a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx index 63aed481548..9f78d84aedd 100644 --- a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx +++ b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx @@ -28,37 +28,38 @@ Here's the code of the Edge Function, you can change the response to handle the ```ts index.ts import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js' +import { withSupabase } from 'npm:@supabase/server@^1' const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? '' const botClient = new WebClient(slackBotToken) console.log(`Slack URL verification function up and running!`) -Deno.serve(async (req) => { - try { - const reqBody = await req.json() - console.log(JSON.stringify(reqBody, null, 2)) - const { token, challenge, type, event } = reqBody - if (type == 'url_verification') { - return new Response(JSON.stringify({ challenge }), { - headers: { 'Content-Type': 'application/json' }, - status: 200, - }) - } else if (event.type == 'app_mention') { - const { user, text, channel, ts } = event - // Here you should process the text received and return a response: - const response = await botClient.chat.postMessage({ - channel: channel, - text: `Hello <@${user}>!`, - thread_ts: ts, - }) - return new Response('ok', { status: 200 }) +// Slack calls this endpoint, so deploy with --no-verify-jwt. +export default { + fetch: withSupabase({ auth: 'none' }, async (req) => { + try { + // Implement your Slack request signature verification here before trusting the payload + // (validate `x-slack-signature` / `x-slack-request-timestamp` with your signing secret). + const reqBody = await req.json() + console.log(JSON.stringify(reqBody, null, 2)) + const { token, challenge, type, event } = reqBody + + if (type == 'url_verification') { + return Response.json({ challenge }) + } else if (event.type == 'app_mention') { + const { user, text, channel, ts } = event + // Here you should process the text received and return a response: + const response = await botClient.chat.postMessage({ + channel: channel, + text: `Hello <@${user}>!`, + thread_ts: ts, + }) + return new Response('ok', { status: 200 }) + } + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }) } - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - headers: { 'Content-Type': 'application/json' }, - status: 500, - }) - } -}) + }), +} ``` diff --git a/apps/docs/content/guides/functions/examples/upstash-redis.mdx b/apps/docs/content/guides/functions/examples/upstash-redis.mdx index 5fdd926f830..630de49aca5 100644 --- a/apps/docs/content/guides/functions/examples/upstash-redis.mdx +++ b/apps/docs/content/guides/functions/examples/upstash-redis.mdx @@ -40,39 +40,43 @@ And add the code to the `index.ts` file: ```ts index.ts import { Redis } from 'https://deno.land/x/upstash_redis@v1.19.3/mod.ts' +import { withSupabase } from 'npm:@supabase/server@^1' console.log(`Function "upstash-redis-counter" up and running!`) -Deno.serve(async (_req) => { - try { - const redis = new Redis({ - url: Deno.env.get('UPSTASH_REDIS_REST_URL')!, - token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!, - }) - - const deno_region = Deno.env.get('DENO_REGION') - if (deno_region) { - // Increment region counter - await redis.hincrby('supa-edge-counter', deno_region, 1) - } else { - // Increment localhost counter - await redis.hincrby('supa-edge-counter', 'localhost', 1) - } - - // Get all values - const counterHash: Record | null = await redis.hgetall('supa-edge-counter') - const counters = Object.entries(counterHash!) - .sort(([, a], [, b]) => b - a) // sort desc - .reduce((r, [k, v]) => ({ total: r.total + v, regions: { ...r.regions, [k]: v } }), { - total: 0, - regions: {}, +// Open endpoint for testing. In production, implement an authorization layer in the handler or switch the auth mode. +export default { + fetch: withSupabase({ auth: 'none' }, async (req, ctx) => { + try { + const redis = new Redis({ + url: Deno.env.get('UPSTASH_REDIS_REST_URL')!, + token: Deno.env.get('UPSTASH_REDIS_REST_TOKEN')!, }) - return new Response(JSON.stringify({ counters }), { status: 200 }) - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { status: 200 }) - } -}) + const deno_region = Deno.env.get('DENO_REGION') + if (deno_region) { + // Increment region counter + await redis.hincrby('supa-edge-counter', deno_region, 1) + } else { + // Increment localhost counter + await redis.hincrby('supa-edge-counter', 'localhost', 1) + } + + // Get all values + const counterHash: Record | null = await redis.hgetall('supa-edge-counter') + const counters = Object.entries(counterHash!) + .sort(([, a], [, b]) => b - a) // sort desc + .reduce((r, [k, v]) => ({ total: r.total + v, regions: { ...r.regions, [k]: v } }), { + total: 0, + regions: {}, + }) + + return Response.json({ counters }) + } catch (error) { + return Response.json({ error: error.message }, { status: 500 }) + } + }), +} ``` ## Run locally diff --git a/apps/docs/content/guides/functions/kysely-postgres.mdx b/apps/docs/content/guides/functions/kysely-postgres.mdx index a8930899f96..0d61b084322 100644 --- a/apps/docs/content/guides/functions/kysely-postgres.mdx +++ b/apps/docs/content/guides/functions/kysely-postgres.mdx @@ -198,6 +198,7 @@ import { PostgresIntrospector, PostgresQueryCompiler, } from 'https://esm.sh/kysely@0.23.4' +import { withSupabase } from 'npm:@supabase/server@^1' import { PostgresDriver } from './DenoPostgresDriver.ts' @@ -245,31 +246,36 @@ const db = new Kysely({ }, }) -Deno.serve(async (_req) => { - try { - // Run a query - const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute() +export default { + fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { + try { + // Run a query + const animals = await db + .selectFrom('animals') + .select(['id', 'animal', 'created_at']) + .execute() - // Neat, it's properly typed \o/ - console.log(animals[0].created_at.getFullYear()) + // Neat, it's properly typed \o/ + console.log(animals[0].created_at.getFullYear()) - // Encode the result as pretty printed JSON - const body = JSON.stringify( - animals, - (key, value) => (typeof value === 'bigint' ? value.toString() : value), - 2 - ) + // Encode the result as pretty printed JSON + const body = JSON.stringify( + animals, + (key, value) => (typeof value === 'bigint' ? value.toString() : value), + 2 + ) - // Return the response with the correct content type header - return new Response(body, { - status: 200, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }) - } catch (err) { - console.error(err) - return new Response(String(err?.message ?? err), { status: 500 }) - } -}) + // Return the response with the correct content type header + return new Response(body, { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + } catch (err) { + console.error(err) + return new Response(String(err?.message ?? err), { status: 500 }) + } + }), +} ``` diff --git a/apps/docs/content/guides/functions/quickstart-dashboard.mdx b/apps/docs/content/guides/functions/quickstart-dashboard.mdx index 4412d30a293..14db8b23f35 100644 --- a/apps/docs/content/guides/functions/quickstart-dashboard.mdx +++ b/apps/docs/content/guides/functions/quickstart-dashboard.mdx @@ -186,14 +186,14 @@ console.log(data) // { message: "Hello JavaScript!" } const response = await fetch('https://[YOUR_PROJECT_ID].supabase.co/functions/v1/hello-world', { method: 'POST', headers: { - Authorization: 'Bearer YOUR_PUBLISHABLE_KEY', + apikey: '', 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'Fetch' }), }) const data = await response.json() -console.log(data) // { message: "Hello Fetch!" } +console.log(data) // { message: "Hello there!" } ``` diff --git a/apps/docs/content/guides/functions/recursive-functions.mdx b/apps/docs/content/guides/functions/recursive-functions.mdx index 580225f4d74..d661924efd1 100644 --- a/apps/docs/content/guides/functions/recursive-functions.mdx +++ b/apps/docs/content/guides/functions/recursive-functions.mdx @@ -97,7 +97,7 @@ Deno.serve(async (req) => { const response = await fetch(`${Deno.env.get('SUPABASE_URL')}/functions/v1/other-function`, { method: 'POST', headers: { - Authorization: `Bearer ${SUPABASE_DEFAULT_PUBLISHABLE_KEY}`, + apikey: SUPABASE_DEFAULT_PUBLISHABLE_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ foo: 'bar' }), @@ -256,7 +256,7 @@ export async function save(data: any) { ```typescript // supabase/functions/process-data/index.ts -import { validate, transform, save } from '../_shared/transform.ts' +import { save, transform, validate } from '../_shared/transform.ts' Deno.serve(async (req) => { const data = await req.json() @@ -284,7 +284,7 @@ async function processWithDelay(items: any[]) { | Pattern | Budget consumption | Recommendation | | ------------------------------- | ------------------ | ----------------- | -| Simple chain (A to B to C) | Low | Generally safe | +| Basic chain (A to B to C) | Low | Generally safe | | Fan-out (A to B, C, D, E) | Moderate | Limit concurrency | | Deep recursion (A to A to A...) | High | Set max depth | | Unbounded loops | Very high | Avoid, use queues | diff --git a/apps/docs/content/guides/functions/routing.mdx b/apps/docs/content/guides/functions/routing.mdx index 89e785455e5..b6c084fe134 100644 --- a/apps/docs/content/guides/functions/routing.mdx +++ b/apps/docs/content/guides/functions/routing.mdx @@ -27,7 +27,7 @@ To combine multiple endpoints into a single Edge Function, you can use web appli ## Basic routing example -Here's a simple hello world example using some popular web frameworks: +Here's a basic hello world example using some popular web frameworks: ```ts -Deno.serve(async (req) => { - if (req.method === 'GET') { +import { withSupabase } from 'npm:@supabase/server@^1' + +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + if (req.method === 'GET') { + return new Response('Hello World!') + } + const { name } = await req.json() + if (name) { + return new Response(`Hello ${name}!`) + } return new Response('Hello World!') - } - const { name } = await req.json() - if (name) { - return new Response(`Hello ${name}!`) - } - return new Response('Hello World!') -}) + }), +} ``` @@ -123,9 +127,11 @@ app.get('/hello-world', (c) => { return new Response('Hello World!') }) -Deno.serve(app.fetch) +export default { fetch: app.fetch } ``` +To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth). + @@ -155,6 +161,8 @@ Keep in mind paths must be prefixed by function name. Route parameters can only ```ts +import { withSupabase } from 'npm:@supabase/server@^1' + interface Task { id: string name: string @@ -204,42 +212,44 @@ async function deleteTask(id: string): Promise { } } -Deno.serve(async (req) => { - const url = new URL(req.url) - const method = req.method - // Extract the last part of the path as the command - const command = url.pathname.split('/').pop() - // Assuming the last part of the path is the task ID - const id = command - try { - switch (method) { - case 'GET': - if (id) { - return getTask(id) - } else { - return getAllTasks() - } - case 'POST': - return createTask(req) - case 'PUT': - if (id) { - return updateTask(id, req) - } else { - return new Response('Bad Request', { status: 400 }) - } - case 'DELETE': - if (id) { - return deleteTask(id) - } else { - return new Response('Bad Request', { status: 400 }) - } - default: - return new Response('Method Not Allowed', { status: 405 }) +export default { + fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { + const url = new URL(req.url) + const method = req.method + // Extract the last part of the path as the command + const command = url.pathname.split('/').pop() + // Assuming the last part of the path is the task ID + const id = command + try { + switch (method) { + case 'GET': + if (id) { + return getTask(id) + } else { + return getAllTasks() + } + case 'POST': + return createTask(req) + case 'PUT': + if (id) { + return updateTask(id, req) + } else { + return new Response('Bad Request', { status: 400 }) + } + case 'DELETE': + if (id) { + return deleteTask(id) + } else { + return new Response('Bad Request', { status: 400 }) + } + default: + return new Response('Method Not Allowed', { status: 405 }) + } + } catch (error) { + return new Response(`Internal Server Error: ${error}`, { status: 500 }) } - } catch (error) { - return new Response(`Internal Server Error: ${error}`, { status: 500 }) - } -}) + }), +} ``` @@ -388,9 +398,11 @@ app.delete('/:id', async (c) => { } }) -Deno.serve(app.fetch) +export default { fetch: app.fetch } ``` +To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth). + @@ -407,6 +419,6 @@ This works well for small apps with only a couple of routes: <$CodeSample path="/edge-functions/supabase/functions/restful-tasks/index.ts" -lines={[[92, 116]]} +lines={[[48, -1]]} meta="restful-tasks/index.ts" /> diff --git a/apps/docs/content/guides/functions/schedule-functions.mdx b/apps/docs/content/guides/functions/schedule-functions.mdx index 1d9a54dc14b..222b938b6a4 100644 --- a/apps/docs/content/guides/functions/schedule-functions.mdx +++ b/apps/docs/content/guides/functions/schedule-functions.mdx @@ -47,7 +47,7 @@ select url:= (select decrypted_secret from vault.decrypted_secrets where name = 'project_url') || '/functions/v1/function-name', headers:=jsonb_build_object( 'Content-type', 'application/json', - 'Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'publishable_key') + 'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'publishable_key') ), body:=concat('{"time": "', now(), '"}')::jsonb ) as request_id; diff --git a/apps/docs/content/guides/functions/status-codes.mdx b/apps/docs/content/guides/functions/status-codes.mdx index e10548aa25a..2674e0205b4 100644 --- a/apps/docs/content/guides/functions/status-codes.mdx +++ b/apps/docs/content/guides/functions/status-codes.mdx @@ -7,6 +7,14 @@ subtitle: 'Understand HTTP status codes returned by Edge Functions to properly d {/* supa-mdx-lint-disable Rule001HeadingCase */} +When invoking an Edge Function, the response may return a variety of HTTP status codes. The most common status codes are listed below. + + + +Error responses may also include an `sb-error-code` header that identifies the specific error condition. See [Error Codes](/docs/guides/functions/error-codes) for a complete list of error codes and their meanings. + + + ## Success Responses ### 2XX Success diff --git a/apps/docs/content/guides/functions/unit-test.mdx b/apps/docs/content/guides/functions/unit-test.mdx index 75f862ee9a6..97689b6f9fd 100644 --- a/apps/docs/content/guides/functions/unit-test.mdx +++ b/apps/docs/content/guides/functions/unit-test.mdx @@ -5,161 +5,199 @@ description: 'Writing Unit Tests for Edge Functions using Deno Test' subtitle: 'Writing Unit Tests for Edge Functions using Deno Test' --- -Testing is an essential step in the development process to ensure the correctness and performance of your Edge Functions. +Testing is an essential step in the development process to ensure the correctness, reliability, and performance of your Edge Functions. Because Edge Functions often combine HTTP handling, authentication, database access, and business logic, a good testing strategy gives you fast feedback and high confidence before deploying to production. + +In this guide you will learn how to write: + +- **Unit tests** for pure business logic such as pricing rules, calculations, etc. +- **Integration tests** for the full Edge Function by mocking at the network layer + +The examples and patterns shown here follow the same approaches used internally by Supabase's Edge Functions team. + +Deno ships with a fast, native test runner and excellent mocking utilities in `@std/testing`. See the [official Deno testing documentation](https://docs.deno.com/runtime/manual/basics/testing/) for more background. --- -## Testing in Deno +## The example scenario -Deno has a built-in test runner that you can use for testing JavaScript or TypeScript code. You can read the [official documentation](https://docs.deno.com/runtime/manual/basics/testing/) for more information and details about the available testing functions. +You can use a realistic Edge Function called `process-ticket` that calculates the final price of a ticket based on the authenticated user's age (loaded from the `profiles` table). + +**Business rules:** + +- Children aged 8 and under → free (`0`) +- Young people aged 9–17 → 20% discount +- Adults aged 18 and over → full price + +The function receives a JSON payload with a `price` field and returns `{ result: finalPrice }`. + +This example demonstrates common real-world requirements: + +- Request validation +- Authenticated database access via `withSupabase` +- Business rule application +- Proper error handling --- -## Folder structure +## Recommended project structure -We recommend creating your testing in a `supabase/functions/tests` directory, using the same name as the Function followed by `-test.ts`: - -```bash -└── supabase - ├── functions - │ ├── function-one - │ │ └── index.ts - │ └── function-two - │ │ └── index.ts - │ └── tests - │ └── function-one-test.ts # Tests for function-one - │ └── function-two-test.ts # Tests for function-two - └── config.toml ``` - ---- - -## Example - -The following script is a good example to get started with testing your Edge Functions: - -```typescript function-one-test.ts -// Import required libraries and modules -import { assert, assertEquals } from 'jsr:@std/assert@1' -import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2' - -// Will load the .env file to Deno.env -import 'jsr:@std/dotenv/load' - -// Set up the configuration for the Supabase client -const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '' -const supabaseKey = Deno.env.get('SUPABASE_PUBLISHABLE_KEY') ?? '' -const options = { - auth: { - autoRefreshToken: false, - persistSession: false, - detectSessionInUrl: false, - }, -} - -// Test the creation and functionality of the Supabase client -const testClientCreation = async () => { - var client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options) - - // Verify if the Supabase URL and key are provided - if (!supabaseUrl) throw new Error('supabaseUrl is required.') - if (!supabaseKey) throw new Error('supabaseKey is required.') - - // Test a simple query to the database - const { data: table_data, error: table_error } = await client - .from('my_table') - .select('*') - .limit(1) - if (table_error) { - throw new Error('Invalid Supabase client: ' + table_error.message) - } - assert(table_data, 'Data should be returned from the query.') -} - -// Test the 'hello-world' function -const testHelloWorld = async () => { - var client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options) - - // Invoke the 'hello-world' function with a parameter - const { data: func_data, error: func_error } = await client.functions.invoke('hello-world', { - body: { name: 'bar' }, - }) - - // Check for errors from the function invocation - if (func_error) { - throw new Error('Invalid response: ' + func_error.message) - } - - // Log the response from the function - console.log(JSON.stringify(func_data, null, 2)) - - // Assert that the function returned the expected result - assertEquals(func_data.message, 'Hello bar!') -} - -// Register and run the tests -Deno.test('Client Creation Test', testClientCreation) -Deno.test('Hello-world Function Test', testHelloWorld) +supabase/ +├── functions/ +│ ├── _shared/ +│ │ └── types.ts # Database types +│ ├── process-ticket/ +│ │ ├── index.ts # Edge Function (uses withSupabase) +│ │ └── pricing.ts # Pure business logic (co-located) +│ └── tests/ +│ ├── utils/ +│ │ └── supabase_env.ts # Test helpers (env + JWT) +│ └── process-ticket/ +│ ├── pricing.test.ts # Unit tests for pricing +│ └── index.test.ts # Integration tests with fetch mocking +├── config.toml +└── deno.json ``` -This test case consists of two parts. - -1. The first part tests the client library and verifies that the database can be connected to and returns values from a table (`my_table`). -2. The second part tests the edge function and checks if the received value matches the expected value. Here's a brief overview of the code: - - We import various testing functions from the Deno standard library, including `assert`, `assertExists`, and `assertEquals`. - - We import the `createClient` and `SupabaseClient` classes from the `@supabase/supabase-js` library to interact with the Supabase client. - - We define the necessary configuration for the Supabase client, including the Supabase URL, API key, and authentication options. - - The `testClientCreation` function tests the creation of a Supabase client instance and queries the database for data from a table. It verifies that data is returned from the query. - - The `testHelloWorld` function tests the "Hello-world" Edge Function by invoking it using the Supabase client's `functions.invoke` method. It checks if the response message matches the expected greeting. - - We run the tests using the `Deno.test` function, providing a descriptive name for each test case and the corresponding test function. - -Make sure to replace the placeholders (`supabaseUrl`, `supabaseKey`, `my_table`) with the actual values relevant to your Supabase setup. +In this reference implementation the pricing logic lives inside the function folder +`process-ticket/pricing.ts`. You can also move it to `_shared/` if you want to reuse it across +multiple functions. +See the [Development Environment](/docs/guides/functions/development-environment) and [Managing dependencies](/docs/guides/functions/dependencies) guides for recommended `deno.json` and editor setup. + --- -## Running Edge Functions locally +## Unit tests: Testing pure business logic -To locally test and debug Edge Functions, you can utilize the Supabase CLI. Let's explore how to run Edge Functions locally using the Supabase CLI: +The pricing rules are pure functions with no side effects, so they are perfect candidates for fast, isolated unit tests. -1. Ensure that the Supabase server is running by executing the following command: +### The pricing module - ```bash - supabase start - ``` +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/process-ticket/pricing.ts" +title="Testing pure business logic | Implementation" +meta="supabase/functions/process-ticket/pricing.ts" +language="typescript" +/> -2. In your terminal, use the following command to serve the Edge Functions locally: +### Unit tests - ```bash - supabase functions serve - ``` +The reference implementation uses the BDD-style API from `@std/testing/bdd`: - This command starts a local server that runs your Edge Functions, enabling you to test and debug them in a development environment. +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/tests/process-ticket/pricing.test.ts" +title="Testing pure business logic | Unit-Test" +meta="supabase/functions/tests/process-ticket/pricing.test.ts" +language="typescript" +/> -3. Create the environment variables file: +Run the unit tests: - ```bash - # creates the file - touch .env - # adds the SUPABASE_URL secret - echo "SUPABASE_URL=http://localhost:54321" >> .env - # adds the SUPABASE_PUBLISHABLE_KEY secret - echo "SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" >> .env - # Alternatively, you can open it in your editor: - open .env - ``` +```bash +deno test supabase/functions/tests/process-ticket/pricing.test.ts +``` -4. To run the tests, use the following command in your terminal: +These tests run in milliseconds and give you immediate safety when changing discount rules. - ```bash - deno test --allow-all supabase/functions/tests/function-one-test.ts - ``` +--- + +## Integration tests: Testing the full Edge Function + +The reference implementation uses a pattern: **mocking `globalThis.fetch`** to intercept the Supabase REST calls made by the Edge Function. This approach requires **zero changes** to your production code for testability. + +### The Edge Function + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/process-ticket/index.ts" +title="Testing the full Edge Function | Implementation" +meta="supabase/functions/process-ticket/index.ts" +language="typescript" +/> + +Key points: + +- Uses the high-level `withSupabase` helper from [`@supabase/server`](https://github.com/supabase/server) +- Automatically provides an authenticated `ctx.supabase` client +- Business logic is delegated to the co-located `pricing.ts` + +### Integration test setup + +This helper sets up a mock Supabase environment and generates valid RS256 JWTs for authenticated requests: + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/tests/utils/supabase_env.ts" +title="Testing the full Edge Function | Unit-Test Utils" +meta="supabase/functions/tests/utils/supabase_env.ts" +language="typescript" +/> + +### Full integration tests + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/tests/process-ticket/index.test.ts" +title="Testing the full Edge Function | Unit-Test" +meta="supabase/functions/tests/process-ticket/index.test.ts" +language="typescript" +/> + +Run the integration tests: + +```bash +deno test supabase/functions/tests/process-ticket/index.test.ts --allow-env +``` + +--- + +## Advantages of mocking approach + +This guide uses `fetch()` mock to demonstrate the following benefits: + +- Test the **real** Edge Function code path — no dependency injection needed in production code +- Simulate database responses, auth failures, network errors +- Keep your production Edge Function clean and focused +- Still get fast, deterministic tests that don't require a running Supabase instance + +This pattern fits great in higher-level helpers that you can control inner code, like `withSupabase`. + +--- + +## Running all tests + +Add to your `deno.json`: + +<$CodeSample +path="/edge-functions/supabase/functions/unit-testing/deno.json" +title="deno.json file" +meta="supabase/deno.json" +language="json" +lines={[[1,1], [6,-1]]} +/> + +Then: + +```bash +deno task test +``` + +--- + +## Best practices + +- Keep pure business logic in separate modules (even if co-located with the function) +- Use `withSupabase` + typed `Database` for clean, authenticated access +- Prefer mocking at the `fetch` boundary for integration tests when you don't want to modify production code +- Use `@std/testing/bdd` + `@std/testing/mock` for expressive, maintainable tests +- Generate realistic JWTs in tests when your function relies on authenticated Supabase clients +- Test both happy paths and error conditions (missing input, DB failures, invalid data) --- ## Resources -- Full guide on Testing Supabase Edge Functions on [Mansueli's tips](https://blog.mansueli.com/testing-supabase-edge-functions-with-deno-test) +- Read the [Deno testing guide](https://docs.deno.com/runtime/manual/basics/testing/) +- Learn more about [`withSupabase` and `@supabase/server`](/blog/introducing-supabase-server) +- See the other Edge Functions guides: [Development Environment](/docs/guides/functions/development-environment), [Managing dependencies](/docs/guides/functions/dependencies), [Deploy to Production](/docs/guides/functions/deploy) diff --git a/apps/docs/content/guides/functions/wasm.mdx b/apps/docs/content/guides/functions/wasm.mdx index c8377bc6e10..fe7e5f61ece 100644 --- a/apps/docs/content/guides/functions/wasm.mdx +++ b/apps/docs/content/guides/functions/wasm.mdx @@ -20,7 +20,7 @@ For example, libraries like [magick-wasm](/docs/guides/functions/examples/image- ### Writing a Wasm module -You can use different languages and SDKs to write Wasm modules. For this tutorial, we will write a simple Wasm module in Rust that adds two numbers. +You can use different languages and SDKs to write Wasm modules. For this tutorial, we will write a basic Wasm module in Rust that adds two numbers. diff --git a/apps/docs/content/guides/functions/websockets.mdx b/apps/docs/content/guides/functions/websockets.mdx index 96bbc615c37..8129b4e2af6 100644 --- a/apps/docs/content/guides/functions/websockets.mdx +++ b/apps/docs/content/guides/functions/websockets.mdx @@ -13,6 +13,8 @@ This allows you to: - Create WebSocket relay servers for external APIs - Establish both incoming and outgoing WebSocket connections +For a production-ready reconnect pattern with session persistence and replay, see [Resumable WebSockets with Edge Functions](/docs/guides/functions/examples/resumable-websockets). + --- ## Creating WebSocket servers @@ -29,26 +31,28 @@ Here are some basic examples of setting up WebSocket servers using Deno and Node ```ts -Deno.serve((req) => { - const upgrade = req.headers.get('upgrade') || '' +export default { + fetch: (req) => { + const upgrade = req.headers.get('upgrade') || '' - if (upgrade.toLowerCase() != 'websocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) - } + if (upgrade.toLowerCase() != 'websocket') { + return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + } - const { socket, response } = Deno.upgradeWebSocket(req) + const { socket, response } = Deno.upgradeWebSocket(req) - socket.onopen = () => console.log('socket opened') - socket.onmessage = (e) => { - console.log('socket message:', e.data) - socket.send(new Date().toString()) - } + socket.onopen = () => console.log('socket opened') + socket.onmessage = (e) => { + console.log('socket message:', e.data) + socket.send(new Date().toString()) + } - socket.onerror = (e) => console.log('socket errored:', e.message) - socket.onclose = () => console.log('socket closed') + socket.onerror = (e) => console.log('socket errored:', e.message) + socket.onclose = () => console.log('socket closed') - return response -}) + return response + }, +} ``` @@ -121,7 +125,7 @@ WebSocket browser clients don't have the option to send custom headers. Because You can skip the default authorization header checks by explicitly providing `--no-verify-jwt` when serving and deploying functions. -To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol. +To authenticate the user making WebSocket requests, you can pass the JWT in URL query params or via a custom protocol. The [`withSupabase`](/docs/guides/functions/auth) wrapper validates credentials on request headers, so it can't authenticate WebSocket clients. Verify the JWT yourself, as shown below. { - const upgrade = req.headers.get('upgrade') || '' - if (upgrade.toLowerCase() != 'WebSocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) - } +export default { + fetch: async (req) => { + const upgrade = req.headers.get('upgrade') || '' + if (upgrade.toLowerCase() != 'websocket') { + return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + } - // Please be aware query params may be logged in some logging systems. - const url = new URL(req.url) - const jwt = url.searchParams.get('jwt') + // Please be aware query params may be logged in some logging systems. + const url = new URL(req.url) + const jwt = url.searchParams.get('jwt') - if (!jwt) { - console.error('Auth token not provided') - return new Response('Auth token not provided', { status: 403 }) - } + if (!jwt) { + console.error('Auth token not provided') + return new Response('Auth token not provided', { status: 403 }) + } - const { error, data } = await supabase.auth.getClaims() + const { error, data } = await supabase.auth.getUser(jwt) - if (error) { - console.error(error) - return new Response('Invalid token provided', { status: 403 }) - } + if (error) { + console.error(error) + return new Response('Invalid token provided', { status: 403 }) + } - if (!data.user) { - console.error('user is not authenticated') - return new Response('User is not authenticated', { status: 403 }) - } + if (!data.user) { + console.error('user is not authenticated') + return new Response('User is not authenticated', { status: 403 }) + } - const { socket, response } = Deno.upgradeWebSocket(req) + const { socket, response } = Deno.upgradeWebSocket(req) - socket.onopen = () => console.log('socket opened') - socket.onmessage = (e) => { - console.log('socket message:', e.data) - socket.send(new Date().toString()) - } + socket.onopen = () => console.log('socket opened') + socket.onmessage = (e) => { + console.log('socket message:', e.data) + socket.send(new Date().toString()) + } - socket.onerror = (e) => console.log('socket errored:', e.message) - socket.onclose = () => console.log('socket closed') + socket.onerror = (e) => console.log('socket errored:', e.message) + socket.onclose = () => console.log('socket closed') - return response -}) + return response + }, +} ``` @@ -197,47 +203,50 @@ const supabase = createClient( SUPABASE_SECRET_KEYS['default'] ) -Deno.serve((req) => { - const upgrade = req.headers.get('upgrade') || '' - if (upgrade.toLowerCase() != 'WebSocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) - } +export default { + fetch: async (req) => { + const upgrade = req.headers.get('upgrade') || '' + if (upgrade.toLowerCase() != 'websocket') { + return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + } - // Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2` - const customProtocols = (req.headers.get('Sec-WebSocket-Protocol') ?? '') - .split(',') - .map((p) => p.trim()) - const jwt = customProtocols.find((p) => p.startsWith('jwt')).replace('jwt-', '') + // Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2` + const customProtocols = (req.headers.get('Sec-WebSocket-Protocol') ?? '') + .split(',') + .map((p) => p.trim()) + const jwtProtocol = customProtocols.find((p) => p.startsWith('jwt-')) + const jwt = jwtProtocol ? jwtProtocol.replace('jwt-', '') : null - if (!jwt) { - console.error('Auth token not provided') - return new Response('Auth token not provided', { status: 403 }) - } + if (!jwt) { + console.error('Auth token not provided') + return new Response('Auth token not provided', { status: 403 }) + } - const { error, data } = await supabase.auth.getClaims() - if (error) { - console.error(error) - return new Response('Invalid token provided', { status: 403 }) - } + const { error, data } = await supabase.auth.getUser(jwt) + if (error) { + console.error(error) + return new Response('Invalid token provided', { status: 403 }) + } - if (!data.user) { - console.error('user is not authenticated') - return new Response('User is not authenticated', { status: 403 }) - } + if (!data.user) { + console.error('user is not authenticated') + return new Response('User is not authenticated', { status: 403 }) + } - const { socket, response } = Deno.upgradeWebSocket(req) + const { socket, response } = Deno.upgradeWebSocket(req) - socket.onopen = () => console.log('socket opened') - socket.onmessage = (e) => { - console.log('socket message:', e.data) - socket.send(new Date().toString()) - } + socket.onopen = () => console.log('socket opened') + socket.onmessage = (e) => { + console.log('socket message:', e.data) + socket.send(new Date().toString()) + } - socket.onerror = (e) => console.log('socket errored:', e.message) - socket.onclose = () => console.log('socket closed') + socket.onerror = (e) => console.log('socket errored:', e.message) + socket.onclose = () => console.log('socket closed') - return response -}) + return response + }, +} ``` @@ -249,6 +258,8 @@ The maximum duration is capped based on the wall-clock, CPU, and memory limits. +When using WebSockets, keep in mind that the HTTP request is considered complete after `Deno.upgradeWebSocket(req)` returns the response. To prevent early worker retirement while the socket is still open, keep an unresolved `EdgeRuntime.waitUntil()` promise that resolves in `socket.onclose`. + --- ## Testing WebSockets locally diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index 45807648272..45a0a7892cc 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -9,43 +9,28 @@ hideToc: true
-
- {[ - { - title: 'Build with AI tools', - description: 'Develop with Supabase AI-first using plugins, MCP, and skills.', - hasLightIcon: true, - href: '/guides/ai', - }, - { - title: 'API Keys', - hasLightIcon: true, - href: '/guides/getting-started/api-keys', - description: 'Learn about the different API keys in Supabase and how to use them.', - }, - { - title: 'Local Development', - hasLightIcon: true, - href: '/guides/cli/getting-started', - description: 'Use the Supabase CLI to develop locally and collaborate between teams.', - } - ].map((resource) => { - return ( - - - {resource.description} - - - ) - -})} - -
+
+ + + Develop with Supabase AI-first using plugins, MCP, and skills. + + + + + Learn about the different API keys in Supabase and how to use them. + + + + + Use the Supabase CLI to develop locally and collaborate between teams. + + +
@@ -54,41 +39,45 @@ hideToc: true ### Use cases
- {[ - { - title: 'AI, Vectors, and embeddings', - href: '/guides/ai#examples', - description: `Build AI-enabled applications using our Vector toolkit.`, - icon: '/docs/img/icons/openai_logo', - hasLightIcon: true, - }, - { - title: 'Subscription Payments (SaaS)', - href: 'https://github.com/vercel/nextjs-subscription-payments#nextjs-subscription-payments-starter', - description: `Clone, deploy, and fully customize a SaaS subscription application with Next.js.`, - icon: '/docs/img/icons/nextjs-icon', - }, - { - title: 'Partner Gallery', - href: 'https://github.com/supabase-community/partner-gallery-example#supabase-partner-gallery-example', - description: `Postgres full-text search, image storage, and more.`, - icon: '/docs/img/icons/nextjs-icon', - }, - ].map((item) => { - return ( - - - {item.description} - - - ) - })} + + + Build AI-enabled applications using our Vector toolkit. + + + + + Clone, deploy, and fully customize a SaaS subscription application with Next.js. + + + + + Postgres full-text search, image storage, and more. + +
### Framework quickstarts @@ -96,130 +85,142 @@ hideToc: true <$Show if="docs:framework_quickstarts">
- {[ - { - title: 'React', - href: '/guides/getting-started/quickstarts/reactjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app.', - icon: '/docs/img/icons/react-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Next.js', - href: '/guides/getting-started/quickstarts/nextjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Next.js app.', - icon: '/docs/img/icons/nextjs-icon', - hasLightIcon: true, - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Nuxt', - href: '/guides/getting-started/quickstarts/nuxtjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Nuxt app.', - icon: '/docs/img/icons/nuxt-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Hono', - href: '/guides/getting-started/quickstarts/hono', - description: - 'Learn how to create a Supabase project, add some sample data to your database, secure it with auth, and query the data from a Hono app.', - icon: '/docs/img/icons/hono-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'RedwoodJS', - href: '/guides/getting-started/quickstarts/redwoodjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database using Prisma migration and seeds, and query the data from a RedwoodJS app.', - icon: '/docs/img/icons/redwood-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Flutter', - href: '/guides/getting-started/quickstarts/flutter', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.', - icon: '/docs/img/icons/flutter-icon', - enabled: isFeatureEnabled('sdk:dart'), - }, - { - title: 'iOS SwiftUI', - href: '/guides/getting-started/quickstarts/ios-swiftui', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app.', - icon: '/docs/img/icons/swift-icon', - enabled: isFeatureEnabled('sdk:swift'), - }, - { - title: 'Android Kotlin', - href: '/guides/getting-started/quickstarts/kotlin', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an Android Kotlin app.', - icon: '/docs/img/icons/kotlin-icon', - enabled: isFeatureEnabled('sdk:kotlin'), - }, - { - title: 'SvelteKit', - href: '/guides/getting-started/quickstarts/sveltekit', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SvelteKit app.', - icon: '/docs/img/icons/svelte-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'SolidJS', - href: '/guides/getting-started/quickstarts/solidjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app.', - icon: '/docs/img/icons/solidjs-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Vue', - href: '/guides/getting-started/quickstarts/vue', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app.', - icon: '/docs/img/icons/vuejs-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'TanStack Start', - href: '/guides/getting-started/quickstarts/tanstack', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a TanStack Start app.', - icon: '/docs/img/icons/tanstack-icon', - hasLightIcon: true, - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Refine', - href: '/guides/getting-started/quickstarts/refine', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Refine app.', - icon: '/docs/img/icons/refine-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - ] - .filter((item) => item.enabled !== false) - .map((item) => { - return ( - - - {item.description} - - - ) - })} + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a React app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Next.js app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Nuxt app. + + + + + Learn how to create a Supabase project, add some sample data to your database, secure it with + auth, and query the data from a Hono app. + + + + + Learn how to create a Supabase project, add some sample data to your database using Prisma + migration and seeds, and query the data from a RedwoodJS app. + + + <$Show if="sdk:dart"> + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Flutter app. + + + + <$Show if="sdk:swift"> + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from an iOS app. + + + + <$Show if="sdk:kotlin"> + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from an Android Kotlin app. + + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a SvelteKit app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a SolidJS app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Vue app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a TanStack Start app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Refine app. + +
@@ -235,90 +236,97 @@ hideToc: true ### Web app demos
- { - [ - { - title: 'Next.js', - href: '/guides/getting-started/tutorials/with-nextjs', - description: - 'Learn how to build a user management app with Next.js and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/nextjs-icon', - hasLightIcon: true, - }, - { - title: 'React', - href: '/guides/getting-started/tutorials/with-react', - description: - 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/react-icon', - }, - { - title: 'Vue 3', - href: '/guides/getting-started/tutorials/with-vue-3', - description: - 'Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/vuejs-icon', - }, - { - title: 'Nuxt 3', - href: '/guides/getting-started/tutorials/with-nuxt-3', - description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/nuxt-icon', - }, - { - title: 'Angular', - href: '/guides/getting-started/tutorials/with-angular', - description: - 'Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/angular-icon', - }, - { - title: 'RedwoodJS', - href: '/guides/getting-started/tutorials/with-redwoodjs', - description: - 'Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/redwood-icon', - }, - { - title: 'Svelte', - href: '/guides/getting-started/tutorials/with-svelte', - description: - 'Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/svelte-icon', - }, - { - title: 'SvelteKit', - href: '/guides/getting-started/tutorials/with-sveltekit', - description: - 'Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/svelte-icon', - }, - { - title: 'Refine', - href: '/guides/getting-started/tutorials/with-refine', - description: - 'Learn how to build a user management app with Refine and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/refine-icon', - } - ] -.map((item) => { - return ( - - - {item.description} - - - ) - -})} - + + + Learn how to build a user management app with Next.js and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Refine and Supabase Database, Auth, and Storage functionality. + +
@@ -327,90 +335,105 @@ hideToc: true ### Mobile tutorials
- {[ - { - title: 'Flutter', - href: '/guides/getting-started/tutorials/with-flutter', - description: - 'Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/flutter-icon', - enabled: isFeatureEnabled('sdk:dart') - }, - { - title: 'Expo React Native', - href: '/guides/getting-started/tutorials/with-expo-react-native', - description: - 'Learn how to build a user management app with Expo React Native and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/expo-icon', - hasLightIcon: true, - enabled: true - }, - { - title: 'Expo React Native Social Auth', - href: '/guides/getting-started/tutorials/with-expo-react-native-social-auth', - description: - 'Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality.', - icon: '/docs/img/icons/expo-icon', - hasLightIcon: true - }, - { - title: 'Android Kotlin', - href: '/guides/getting-started/tutorials/with-kotlin', - description: - 'Learn how to build a product management app with Android and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/kotlin-icon', - enabled: isFeatureEnabled('sdk:kotlin') - }, - { - title: 'iOS Swift', - href: '/guides/getting-started/tutorials/with-swift', - description: - 'Learn how to build a user management app with iOS and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/swift-icon', - enabled: isFeatureEnabled('sdk:swift') - }, - { - title: 'Ionic React', - href: '/guides/getting-started/tutorials/with-ionic-react', - description: - 'Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon', - enabled: true - }, - { - title: 'Ionic Vue', - href: '/guides/getting-started/tutorials/with-ionic-vue', - description: - 'Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon', - enabled: true - }, - { - title: 'Ionic Angular', - href: '/guides/getting-started/tutorials/with-ionic-angular', - description: - 'Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon', - enabled: true - } - ] -.filter((item) => item.enabled !== false) -.map((item) => { - return ( - - - {item.description} - - - ) - -})} - + <$Show if="sdk:dart"> + + + Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality. + + + + + + Learn how to build a user management app with Expo React Native and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality. + + + <$Show if="sdk:kotlin"> + + + Learn how to build a product management app with Android and Supabase Database, Auth, and Storage functionality. + + + + <$Show if="sdk:swift"> + + + Learn how to build a user management app with iOS and Supabase Database, Auth, and Storage functionality. + + + + + + Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality. + +
diff --git a/apps/docs/content/guides/getting-started/api-keys.mdx b/apps/docs/content/guides/getting-started/api-keys.mdx index 1d3ebea3115..3ca3976f52a 100644 --- a/apps/docs/content/guides/getting-started/api-keys.mdx +++ b/apps/docs/content/guides/getting-started/api-keys.mdx @@ -32,15 +32,7 @@ There are 4 types of API keys that you can use with Supabase: | `anon` | JWT (long lived) | Low | Platform, CLI | Legacy version of publishable keys. | | `service_role` | JWT (long lived) | Elevated | Platform, CLI | Legacy version of secret keys. | - - -Supabase has changed the way keys work to improve project security and developer experience. You can read [the full announcement](https://github.com/orgs/supabase/discussions/29260). - -`anon` and `service_role` keys are based on the project's JWT secret. They are generated when your project is created and you can only change them when you rotate the JWT secret. This can cause significant issues in production applications. **You should now use the `sb_publishable_xxx` and `sb_secret_xxx` keys instead**. - -You can still find legacy keys in the **Legacy anon, service_role API keys** tab of the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard: - - +<$Partial path="api_keys_deprecation.mdx" /> ## Publishable keys @@ -97,7 +89,7 @@ Never expose your secret keys publicly. Your data is at risk. **Do not:** - Never use in a browser, even on `localhost`. - Do not pass in URLs or query params, as these are often logged. - Be careful passing them in request headers without prior log sanitization. -- Take extra care logging even potentially **invalid API keys**. Simple typos might reveal the real key in the future. +- Take extra care logging even potentially **invalid API keys**. Typos might reveal the real key in the future. - Reveal, copy, use or manipulate on hardware devices without full disk encryption and which you do not directly own or control (such as public computers, friend's laptop, etc.) Ensure you handle them with care and using [secure coding practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/stable-en/). diff --git a/apps/docs/content/guides/getting-started/architecture.mdx b/apps/docs/content/guides/getting-started/architecture.mdx index e75cd4ab38b..0e8767f5c3a 100644 --- a/apps/docs/content/guides/getting-started/architecture.mdx +++ b/apps/docs/content/guides/getting-started/architecture.mdx @@ -4,7 +4,7 @@ description: 'Supabase design and architecture' tocVideo: 'T-qAtAKjqwc' --- -Supabase is open source. We choose open source tools which are scalable and make them simple to use. +Supabase is open source. We choose open source tools which are scalable and make them approachable. Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way: our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch. @@ -13,7 +13,7 @@ Most notably, we use Postgres rather than a NoSQL store. This choice was deliber ## Choose your comfort level -Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine. +Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you want to treat Postgres like a basic table-store, that’s perfectly fine. ## Architecture diff --git a/apps/docs/content/guides/getting-started/features.mdx b/apps/docs/content/guides/getting-started/features.mdx index 184be241615..d27f2ca8a6d 100644 --- a/apps/docs/content/guides/getting-started/features.mdx +++ b/apps/docs/content/guides/getting-started/features.mdx @@ -34,7 +34,7 @@ Encrypt sensitive data and store secrets using our Postgres extension, Supabase ### Replication -Automatically replicate your database to external destinations like data warehouses and analytics platforms. Read [the external replication documentation](/docs/guides/database/replication/external-replication-setup) for more details. +Automatically replicate your database to destination systems like data warehouses and analytics platforms with external replication (ETL), powered by Supabase ETL. Read [the documentation](/docs/guides/database/replication/external-replication-setup) for more details. ## Platform @@ -128,7 +128,7 @@ Helpers for implementing user authentication in popular server-side languages an ### File storage -Supabase Storage makes it simple to store and serve files. [Docs](/docs/guides/storage). +Supabase Storage helps you store and serve files. [Docs](/docs/guides/storage). ### Content Delivery Network diff --git a/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx b/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx new file mode 100644 index 00000000000..8ad184491b2 --- /dev/null +++ b/apps/docs/content/guides/getting-started/migrating-to-new-api-keys.mdx @@ -0,0 +1,219 @@ +--- +id: 'migrating-to-new-api-keys' +title: 'Migrating to publishable and secret API keys' +description: 'Move from legacy JWT-based anon and service_role keys to publishable and secret keys.' +--- + +<$Partial path="api_keys_deprecation.mdx" /> + +This guide covers migrating an **existing project.** Both key types work simultaneously, so you can swap clients one at a time and deactivate the legacy keys only after nothing depends on them. + +## Before you start + +The migration maps onto your existing keys: + +| Legacy key | Replace with | Used by | +| -------------- | --------------- | ------------------------------------------------------ | +| `anon` | Publishable key | Browsers, mobile and desktop apps, CLIs, public source | +| `service_role` | Secret key | Servers, Edge Functions, workers, other backend code | + +For a full explanation of each key type, read [the Understanding API keys guide](/docs/guides/getting-started/api-keys). + +## Step 1: Create the new API keys + +Open the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard and select the **Publishable and secret API keys** tab. + +Older projects don't have these keys yet. If you see a **Create new API keys** button, your project is still on legacy keys only. Creating the new keys is safe. It adds a publishable key and a secret key alongside your existing `anon` and `service_role` keys. Your legacy keys keep working. + +The new keys are created under the name `default`. You can add more keys with different names later, for example, one secret key per backend component, so you can rotate them independently. For an initial migration, the `default` publishable and secret keys are all you need. + +## Step 2: Swap the publishable key in client code + +Anywhere you use the `anon` key in public code, switch to the publishable key. This includes web pages, mobile and desktop apps, and any CLI or script that ships to users. + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient( + 'https://your-project.supabase.co', + 'sb_publishable_...' // was the anon key +) +``` + +The publishable key carries the same low privileges as the `anon` key, so your [Row Level Security](/docs/guides/database/postgres/row-level-security) policies behave the same. User authentication through Supabase Auth is unchanged. The user still signs in and gets their own JWT. + +## Step 3: Swap the secret key in backend code + +Anywhere you use the `service_role` key on a server you control, switch to a secret key. + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabaseAdmin = createClient( + 'https://your-project.supabase.co', + 'sb_secret_...' // was the service_role key +) +``` + +Secret keys add protections the `service_role` key doesn't have. They return HTTP 401 if used in a browser (matched on the `User-Agent` header), and you can run a separate key per service so a single leak only forces one rotation. + + + +Secret keys bypass Row Level Security and have full access to your data. Keep them on backends you control, out of source control, and out of client code. + + + +### Database Webhooks and `pg_net` + +Calls made from Postgres with `pg_net`, including Database Webhooks, usually send the `service_role` key on the `Authorization: Bearer` header. The new secret keys aren't JWTs, so they're rejected there. Send the secret key on the `apikey` header instead. + +```sql +-- before +select net.http_post( + url := 'https://your-project.supabase.co/functions/v1/your-function', + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer ' + ), + body := jsonb_build_object('event', 'ping') +); + +-- after +select net.http_post( + url := 'https://your-project.supabase.co/functions/v1/your-function', + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'apikey', 'sb_secret_...' + ), + body := jsonb_build_object('event', 'ping') +); +``` + +For Database Webhooks created in the Dashboard, edit each webhook's HTTP headers: remove the `Authorization` header that holds the key and add an `apikey` header with a secret key instead. + + + +Don't hardcode a secret key in SQL or a webhook configuration, where it's stored in plain text. Store it in [Vault](/docs/guides/database/vault) and read it at call time: + +```sql +headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'secret_key') +) +``` + + + +## Step 4: Update Edge Functions + +Edge Functions read their keys from environment variables. Supabase adds two new ones to your functions' environment, `SUPABASE_PUBLISHABLE_KEYS` and `SUPABASE_SECRET_KEYS`, alongside the legacy `SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY`. Confirm they exist in the [**Edge Functions > Secrets**](/dashboard/project/_/functions/secrets) section of the Dashboard before you start. + +You have two options: a minimal change that swaps which variable you read, or a fuller upgrade to the [`@supabase/server`](https://github.com/supabase/server) SDK. + +### Option 1: Read the new keys from the environment + +For most functions, the only change is how you read the key. The legacy variables held a plain string. The new ones hold a JSON object keyed by name, so you parse them and read the key by name. The key you created in [step 1](#step-1-create-the-new-api-keys) is named `default`. + +```ts +// before +const secretKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! + +// after +const secretKey = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default'] +``` + +The publishable keys work the same way through `SUPABASE_PUBLISHABLE_KEYS`. Read [the Managing Secrets guide](/docs/guides/functions/secrets) for more on environment variables in Edge Functions. + +If you created more than one secret key in [step 1](#step-1-create-the-new-api-keys), every key lives in the same `SUPABASE_SECRET_KEYS` object, each under its own name. Read a non-default key the same way: + +```ts +const secretKeys = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) + +const defaultKey = secretKeys['default'] +const billingKey = secretKeys['billing'] // the secret key you named "billing" +``` + +Send publishable and secret keys on the `apikey` header only. If you also pass the key on the `Authorization: Bearer` header, which many Supabase clients do by default, the platform tries to parse it as a JWT and rejects the request with `Invalid JWT`. The platform's built-in `verify_jwt` check only understands the legacy JWT-based keys, so set `verify_jwt = false` for these functions and authorize the request in your own code, or let the `@supabase/server` SDK do it for you ([Option 2](#option-2-adopt-the-supabaseserver-sdk)). + +```toml +[functions.my-function] +verify_jwt = false +``` + +### Option 2: Adopt the @supabase/server SDK + +To get the most out of the new key model, migrate to the [`@supabase/server`](https://github.com/supabase/server) SDK. It removes the client-setup boilerplate every function repeats: reading keys from the environment, parsing the `Authorization` header, and initializing a user-scoped client and a separate admin client. You declare who can call the function, and get both clients ready to use on `ctx` (`ctx.supabase` respects Row Level Security, `ctx.supabaseAdmin` uses the secret key). This is the recommended approach for new functions. + +Wrap your existing `Deno.serve` handler with `withSupabase` and declare an `auth` mode for who can call it. Keep `verify_jwt = false` so the SDK does the authorization. + +For a function your users call from the client, use `auth: 'user'`. The SDK validates the user's session JWT and gives you a client scoped to their Row Level Security policies. + +```ts +import { withSupabase } from 'npm:@supabase/server' + +Deno.serve( + withSupabase({ auth: 'user' }, async (_req, ctx) => { + // ctx.supabase is scoped to the authenticated user + return Response.json({ email: ctx.userClaims?.email }) + }) +) +``` + +For a function called by your own backend, a worker, or `pg_net`, use `auth: 'secret'`. The SDK validates the secret key and gives you a client that bypasses Row Level Security. + +```ts +import { withSupabase } from 'npm:@supabase/server' + +Deno.serve( + withSupabase({ auth: 'secret' }, async (_req, ctx) => { + // ctx.supabaseAdmin is authenticated with a valid secret key + return Response.json({ ok: true }) + }) +) +``` + +To accept a specific named key instead of `default`, add its name after the mode with a colon. For example, `auth: 'secret:billing'` validates the request against the secret key you named `billing`, and `auth: 'publishable:web'` against a publishable key named `web`. + +`withSupabase` returns a standard request handler, so you can also export it as a `fetch` handler instead of passing it to `Deno.serve`: + +```ts +import { withSupabase } from 'npm:@supabase/server' + +export default { + fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { + // ctx.supabase is scoped to the authenticated user + return Response.json({ email: ctx.userClaims?.email }) + }), +} +``` + +`export default { fetch }` is equivalent to `Deno.serve(...)`: both define a request handler. The `fetch` style is portable across Edge Functions, Cloudflare Workers, and Bun, so prefer it if you want the same function to run in more than one environment. `Deno.serve` keeps working on Edge Functions, so you can leave it in place during a migration and switch later. + +A good way to try this is to duplicate one of your functions and migrate the copy first. See [Securing Edge Functions](/docs/guides/functions/auth) for every auth mode and use case, and [Authorization headers](/docs/guides/functions/auth-headers) for how the headers work. + +## Step 5: Verify nothing uses the legacy keys + +Before turning the legacy keys off, confirm nothing still depends on them. There's no automatic usage indicator, so this is a manual check. Go through every place that holds a Supabase key and make sure it now uses a publishable or secret key. + +Don't forget callers that are easy to miss: + +- Mobile or desktop app versions already in users' hands. +- CI/CD pipelines and deployment scripts. +- Third-party integrations and webhooks. +- Cron jobs, workers, and `pg_net` calls or Database Webhooks (see [Database Webhooks and `pg_net`](#database-webhooks-and-pgnet)). + +## Step 6: Deactivate the legacy keys + +Once you've confirmed nothing uses the legacy keys, deactivate them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard. You can re-activate them if you find a client you missed, so this step is reversible. + +## Known limitations + +A few behaviors differ from the legacy JWT-based keys. Plan for them during the migration: + +- You can't send a publishable or secret key in the `Authorization: Bearer ...` header. Send it on the `apikey` header instead. +- Edge Functions don't verify the `apikey` header for the new keys. Use `verify_jwt = false` and authorize in code, as shown in [Step 4](#step-4-update-edge-functions). +- Public Realtime connections are limited to 24 hours unless the connection is upgraded with user-level authentication through Supabase Auth or a supported third-party auth provider. + +## Next steps + +After migrating your keys, consider moving to the [JWT signing keys](/docs/guides/auth/signing-keys) system as well. This is a separate, independent migration. The new publishable and secret keys aren't JWTs, so they no longer touch your project's JWT secret. But the access tokens Supabase Auth issues to your users are still signed by that shared secret. Signing keys replace it with rotatable keys you can change without downtime. Together, the two migrations get your whole project off the shared JWT secret. diff --git a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx index c68f7cb7c67..498936ca67f 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx @@ -73,7 +73,7 @@ hideToc: true await Supabase.initialize( url: 'YOUR_SUPABASE_URL', - anonKey: 'YOUR_SUPABASE_PUBLISHABLE_KEY', + publishableKey: 'YOUR_SUPABASE_PUBLISHABLE_KEY', ); runApp(MyApp()); } diff --git a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx index 381caecf2e5..6e7744279bf 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx @@ -29,7 +29,7 @@ hideToc: true - Install [Laravel Breeze](https://laravel.com/docs/10.x/starter-kits#laravel-breeze), a simple implementation of all of Laravel's [authentication features](https://laravel.com/docs/10.x/authentication). + Install [Laravel Breeze](https://laravel.com/docs/10.x/starter-kits#laravel-breeze), a basic implementation of all of Laravel's [authentication features](https://laravel.com/docs/10.x/authentication). diff --git a/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx index 7f6a4f66a68..661521fefbc 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx @@ -143,9 +143,9 @@ hideToc: true - Let's seed the database with a few instruments. + Seed the database with a few instruments. - Update the file `scripts/seeds.ts` to contain the following code: + Update the file `scripts/seed.ts` to contain the following code: diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index 16fff4527d8..30be6126edc 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -112,7 +112,7 @@ hideToc: true - Use `load` method to fetch the data server-side and display the query results as a simple list. + Use `load` method to fetch the data server-side and display the query results as a list. Create `+page.server.js` file in the `src/routes` directory with the following code. diff --git a/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx b/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx index 257064c7481..9f9d8b943f5 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-flutter.mdx @@ -18,7 +18,7 @@ If you get stuck while working through this guide, refer to the [full example on ## Building the app -Let's start building the Flutter app from scratch. +Build the Flutter app from scratch. ### Initialize a Flutter app @@ -29,7 +29,7 @@ an app called `supabase_quickstart`: flutter create supabase_quickstart ``` -Then let's install the only additional dependency: [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) +Then install the only additional dependency: [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) Copy and paste the following line in your pubspec.yaml to install the package: @@ -41,7 +41,7 @@ Run `flutter pub get` to install the dependencies. ### Setup deep links -Now that we have the dependencies installed let's setup deep links. +With dependencies installed, set up deep links. Setting up deep links is required to bring back the user to the app when they click on the magic link to sign in. We can setup deep links with just a minor tweak on our Flutter application. @@ -67,7 +67,7 @@ Add `CFBundleURLTypes` to enable deep linking: <$CodeTabs> -```xml name=ios/Runner/Info.plist" +```xml name=ios/Runner/Info.plist @@ -144,7 +144,7 @@ void main() { ### Main function -Now that we have deep links ready let's initialize the Supabase client inside our `main` function with the API credentials that you copied [earlier](#get-the-api-keys). These variables will be exposed on the app, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. +With deep links configured, initialize the Supabase client inside the `main` function with the API credentials that you copied [earlier](#get-the-api-keys). These variables will be exposed on the app, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. <$CodeTabs> @@ -191,7 +191,7 @@ Notice that we have a `showSnackBar` extension method that we will use to show s ### Set up a login page -Let's create a Flutter widget to manage logins and sign ups. We will use Magic Links, so users can sign in with their email without using passwords. +Create a Flutter widget to manage logins and sign ups. We will use Magic Links, so users can sign in with their email without using passwords. Notice that this page sets up a listener on the user's auth state using `onAuthStateChange`. A new event will fire when the user comes back to the app by clicking their magic link, which this page can catch and redirect the user accordingly. @@ -310,11 +310,11 @@ class _LoginPageState extends State { ### Set up account page After a user is signed in we can allow them to edit their profile details and manage their account. -Let's create a new widget called `account_page.dart` for that. +Create a new widget called `account_page.dart`. <$CodeTabs> -```dart name=lib/pages/account_page.dart" +```dart name=lib/pages/account_page.dart import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_quickstart/main.dart'; @@ -459,7 +459,7 @@ class _AccountPageState extends State { ### Launch! -Now that we have all the components in place, let's update `lib/main.dart`. +With all components in place, update `lib/main.dart`. The `home` of the `MaterialApp`, meaning the initial page shown to the user, will be the `LoginPage` if the user is not authenticated, and the `AccountPage` if the user is authenticated. We also included some theming to make the app look a bit nicer. @@ -569,7 +569,7 @@ Once you are done with all of the above, it is time to dive into coding. ### Create an upload widget -Let's create an avatar for the user so that they can upload a profile photo. +Create an avatar so the user can upload a profile photo. We can start by creating a new component: <$CodeTabs> diff --git a/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx b/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx index 0148facc6db..a1bec2c404b 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-ionic-angular.mdx @@ -125,7 +125,7 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma ### Create an upload widget -Let's create an avatar for the user so that they can upload a profile photo. +Create an avatar so the user can upload a profile photo. First, install two packages in order to interact with the user's camera. diff --git a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx index 82e348a086d..f7fd26e7894 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx @@ -268,7 +268,7 @@ npm run dev And then open the browser to [localhost:3000/login](http://localhost:3000/login) and you should see the completed app. -When you enter your email and password, you will receive an email with the title **Confirm Your Signup**. Congrats 🎉!!! +When you enter your email and password, you will receive an email with the title **Confirm your email**. Congrats 🎉!!! At this stage you have a fully functional application! diff --git a/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx b/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx index fc1bdb68c77..4541d2fc44b 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nuxt-3.mdx @@ -18,7 +18,7 @@ If you get stuck while working through this guide, you can find the [full exampl ## Building the app -Let's start building the Vue 3 app from scratch. +Build the Vue 3 app from scratch. ### Initialize a Nuxt 3 app @@ -30,7 +30,7 @@ npx nuxi init nuxt-user-management cd nuxt-user-management ``` -Then let's install the only additional dependency: [Nuxt Supabase](https://supabase.nuxtjs.org/). We only need to import Nuxt Supabase as a dev dependency. +Then install the only additional dependency: [Nuxt Supabase](https://supabase.nuxtjs.org/). We only need to import Nuxt Supabase as a dev dependency. ```bash npm install @nuxtjs/supabase --save-dev @@ -73,7 +73,7 @@ export default defineNuxtConfig({ ### Set up Auth component -Let's set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. +Set up a Vue component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. <$CodeTabs> @@ -128,7 +128,7 @@ To access the user information, use the composable [`useSupabaseUser`](https://s ### Account component After a user is signed in we can allow them to edit their profile details and manage their account. -Let's create a new component for that called `Account.vue`. +Create a new component called `Account.vue`. <$CodeTabs> @@ -360,3 +360,43 @@ And then open the browser to [localhost:3000](http://localhost:3000) and you sho ![Supabase Nuxt 3](/docs/img/supabase-vue-3-demo.png) At this stage you have a fully functional application! + +## Add a server route + +So far the app authenticates the user on the client. For protected API endpoints or server-rendered data, you need a server route that verifies the session. + +[`@supabase/server`](https://supabase.github.io/server/) handles the full flow through a single middleware: it validates the JWT locally (using your project's asymmetric signing keys, no round-trip to the Auth server), attaches an RLS-scoped Supabase client and the user's claims to the request, and rejects unauthenticated requests with a 401 before your handler runs. + +```bash +npm install @supabase/server +``` + +<$CodeTabs> + +```typescript name=server/api/profile.get.ts +import { withSupabase } from '@supabase/server/adapters/h3' +import { defineHandler } from 'h3' + +export default defineHandler({ + middleware: [withSupabase({ auth: 'user' })], + handler: async (event) => { + const { supabase, userClaims } = event.context.supabaseContext + + const { data, error } = await supabase + .from('profiles') + .select('username, website, avatar_url') + .eq('id', userClaims.id) + .single() + + if (error) { + throw createError({ statusCode: 500, statusMessage: error.message }) + } + + return data + }, +}) +``` + + + +For an unauthenticated route, pass `auth: 'none'`. For app-wide auth, register `withSupabase({ auth: 'user' })` as a Nuxt server middleware at `server/middleware/supabase.ts` instead. See the [h3/Nuxt adapter docs](https://supabase.github.io/server/adapters/h3) for typing, route overrides, and the full API. diff --git a/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx index 18f4af41521..7ff85870f22 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx @@ -28,7 +28,7 @@ Important: When this guide refers to "API," that means the Supabase API and when The **`api side`** is an implementation of a GraphQL API. The business logic is organized into "services" that represent their own internal API and can be called both from external GraphQL requests and other internal services. -The **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React "Page" components (and automatically code-split your app on each route). +The **`web side`** is built with React. Redwood's router lets you map URL paths to React "Page" components (and automatically code-split your app on each route). Pages may contain a "Layout" component to wrap content. They also contain "Cells" and regular React components. Cells allow you to declaratively manage the lifecycle of a component that fetches and displays data. @@ -43,7 +43,7 @@ to how your Supabase `public` schema references the `auth.users`. ## Building the app -Let's start building the RedwoodJS app from scratch. +Build the RedwoodJS app from scratch. @@ -77,7 +77,7 @@ While the app is installing, you should see: Thanks for trying out Redwood! ``` -Then let's install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command: +Then install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command: ```bash yarn redwood setup auth supabase @@ -174,7 +174,7 @@ You can find the full contents of this file [in the example repository](https:// ### Start RedwoodJS and your first page -Let's test our setup at the moment by starting up the app: +Test your setup by starting the app: ```bash yarn rw dev @@ -188,7 +188,7 @@ yarn rw dev You should see a "Welcome to RedwoodJS" page and a message about not having any pages yet. -So, let's create a "home" page: +Create a "home" page: ```bash yarn rw generate page home / @@ -232,7 +232,7 @@ export default Routes ### Set up a login component -Let's set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. +Set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. ```bash yarn rw g component auth @@ -249,8 +249,8 @@ Now, update the `Auth.js` component to contain: <$CodeTabs> ```jsx name=/web/src/components/Auth/Auth.js -import { useState } from 'react' import { useAuth } from '@redwoodjs/auth' +import { useState } from 'react' const Auth = () => { const { logIn } = useAuth() @@ -310,7 +310,7 @@ export default Auth After a user is signed in we can allow them to edit their profile details and manage their account. -Let's create a new component for that called `Account.js`. +Create a new component called `Account.js`. ```bash yarn rw g component account @@ -326,8 +326,8 @@ And then update the file to contain: <$CodeTabs> ```jsx name=web/src/components/Account/Account.js -import { useState, useEffect } from 'react' import { useAuth } from '@redwoodjs/auth' +import { useEffect, useState } from 'react' const Account = () => { const { client: supabase, currentUser, logOut } = useAuth() @@ -464,7 +464,6 @@ With all the components in place, update your `HomePage` page to use them: ```jsx name=web/src/pages/HomePage/HomePage.js import { useAuth } from '@redwoodjs/auth' import { MetaTags } from '@redwoodjs/web' - import Account from 'src/components/Account' import Auth from 'src/components/Auth' @@ -496,7 +495,7 @@ Next, add a way for users to upload a profile photo. Supabase configures every p ### Create an upload widget -Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component: +Create an avatar so the user can upload a profile photo. Start by creating a new component: ```bash yarn rw g component avatar @@ -511,8 +510,8 @@ Now, update your Avatar component to contain the following widget: <$CodeTabs> ```jsx name=web/src/components/Avatar/Avatar.js -import { useEffect, useState } from 'react' import { useAuth } from '@redwoodjs/auth' +import { useEffect, useState } from 'react' const Avatar = ({ url, size, onUpload }) => { const { client: supabase } = useAuth() diff --git a/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx b/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx index 7cda94c9d61..064a8c2c732 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-solidjs.mdx @@ -151,3 +151,47 @@ And then open the browser to [localhost:3000](http://localhost:3000) and you sho ![Supabase SolidJS](/docs/img/supabase-solidjs-demo.png) At this stage you have a fully functional application! + +## Add a server route (SolidStart) + +The example above is client-only. If you migrate the app to [SolidStart](https://start.solidjs.com/) for server-side rendering and API routes, you can add protected server endpoints with [`@supabase/server`](https://supabase.github.io/server/). + +`createSupabaseContext` validates the incoming request's JWT locally (using your project's asymmetric signing keys, no round-trip to the Auth server), scopes a Supabase client to the authenticated user via RLS, and exposes the user's claims, all from a single call inside your SolidStart API route handler. + +```bash +npm install @supabase/server +``` + +<$CodeTabs> + +```typescript name=src/routes/api/profile.ts +import type { APIEvent } from '@solidjs/start/server' +import { createSupabaseContext } from '@supabase/server' + +export async function GET({ request }: APIEvent) { + const { data: ctx, error } = await createSupabaseContext(request, { + auth: 'user', + }) + + if (error) { + return Response.json({ message: error.message, code: error.code }, { status: error.status }) + } + + const { supabase, userClaims } = ctx + const { data, error: queryError } = await supabase + .from('profiles') + .select('username, website, avatar_url') + .eq('id', userClaims.id) + .single() + + if (queryError) { + return Response.json({ message: queryError.message }, { status: 500 }) + } + + return Response.json(data) +} +``` + + + +To make a route public, swap `auth: 'user'` for `auth: 'none'`. For app-wide authentication via SolidStart middleware, or for the full `@supabase/server` API, see the [getting started guide](https://supabase.github.io/server/getting-started). diff --git a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx index f46e2edd0bd..0e6c1728038 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-sveltekit.mdx @@ -78,7 +78,7 @@ meta="name=src/hooks.server.ts" <$Partial path="get_session_warning.mdx" /> {/* TODO: Change when adding JS autoconversion */} -As this tutorial uses TypeScript the compiler complains about `event.locals.supabase` and `event.locals.safeGetSession`, you can fix this by updating the `src/app.d.ts` with the content below: +As this tutorial uses TypeScript the compiler complains about `event.locals.supabase`. You can fix this by updating the `src/app.d.ts` with the content below: <$CodeTabs> diff --git a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx index 4e240e1d969..45057d067c1 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx @@ -17,7 +17,7 @@ If you get stuck while working through this guide, you can find the [full exampl ## Building the app -Let's start building the SwiftUI app from scratch. +Build the SwiftUI app from scratch. ### Create a SwiftUI app in Xcode @@ -281,7 +281,7 @@ Next, add a way for users to upload a profile photo. Supabase configures every p ### Add `PhotosPicker` -Let's add support for the user to pick an image from the library and upload it. +Add support for the user to pick an image from the library and upload it. Start by creating a new type to hold the picked avatar image: <$CodeTabs> diff --git a/apps/docs/content/guides/integrations/partner-integration-guide.mdx b/apps/docs/content/guides/integrations/partner-integration-guide.mdx index 44cc2915556..3cf4bd56726 100644 --- a/apps/docs/content/guides/integrations/partner-integration-guide.mdx +++ b/apps/docs/content/guides/integrations/partner-integration-guide.mdx @@ -9,52 +9,51 @@ This guide assumes you have already followed [the Build a Supabase Integration g When a Supabase user clicks the **Install Integration** button in the Dashboard, they are redirected to your system to begin the OAuth flow. You can implement this redirect in one of two ways: -- **[Simple redirect](#method-1-simple-redirect)**: Easier to build, but your system cannot verify that the incoming user was sent by Supabase. +- **[Redirect](#method-1-redirect)**: Easier to build, but your system cannot verify that the incoming user was sent by Supabase. - **[Signed redirect](#method-2-signed-redirect)**: More work to build, but cryptographically verifies that the redirect originated from Supabase. **Recommended for production integrations.** Pick the method that fits your security requirements, then follow the matching section below. -## Method 1: Simple redirect +## Method 1: Redirect In this method, you implement a single `GET` endpoint. Supabase redirects the user to this endpoint when they click **Install Integration**, and your endpoint kicks off the OAuth flow. ->Browser: Clicks "Install Integration" + User->>Browser: Clicks "Install Integration" - Browser->>Partner: "Connect to Partner" Click Handler Page - Partner->>Browser: Redirect to Supabase Authorization Page + Browser->>Partner: "Connect to Partner" Click Handler Page + Partner->>Browser: Redirect to Supabase Authorization Page - Browser->>Supabase: Request Authorization - Supabase->>Browser: Redirect to Consent Screen + Browser->>Supabase: Request Authorization + Supabase->>Browser: Redirect to Consent Screen - Browser->>User: Display Consent Screen - User->>Browser: Gives Consent + Browser->>User: Display Consent Screen + User->>Browser: Gives Consent - Browser->>Supabase: Submit User Consent - Supabase->>Browser: Redirect to Partner With Authorization Code + Browser->>Supabase: Submit User Consent + Supabase->>Browser: Redirect to Partner With Authorization Code - Browser->>Partner: Submit Authorization Code + Browser->>Partner: Submit Authorization Code - Partner->>Supabase: Exchange Authorization Code for Token - Supabase->>Partner: Return Token + Partner->>Supabase: Exchange Authorization Code for Token + Supabase->>Partner: Return Token - Partner->>Supabase: Request Management API Resources - Supabase->>Partner: Return Resources + Partner->>Supabase: Request Management API Resources + Supabase->>Partner: Return Resources - Partner->>Browser: Ok 200, OAuth Flow Complete + Partner->>Browser: Ok 200, OAuth Flow Complete - Browser->>User: Display "Integration Installed" Message`} - -/> + Browser->>User: Display "Integration Installed" Message +``` ### Step 1: Implement the redirect endpoint @@ -88,55 +87,54 @@ You will implement two endpoints: 1. A `POST` endpoint that validates the signed JWT and returns a unique redirect URL. 2. A `GET` endpoint that handles the user once they are redirected to that URL. ->Browser: Clicks "Install Integration" + User->>Browser: Clicks "Install Integration" - Browser->>Supabase: Request Redirect URL Generation - Supabase->>Partner: Send Signed JWT - Partner->>Partner: Validate JWT - Partner->>Partner: Generate Unique Redirect URL - Partner->>Supabase: Return Redirect URL - Supabase->>Browser: Redirect to the Redirect URL + Browser->>Supabase: Request Redirect URL Generation + Supabase->>Partner: Send Signed JWT + Partner->>Partner: Validate JWT + Partner->>Partner: Generate Unique Redirect URL + Partner->>Supabase: Return Redirect URL + Supabase->>Browser: Redirect to the Redirect URL - Browser->>Partner: Visit Redirect URL - Partner->>Partner: Validate Redirect Record - Partner->>Browser: Optional User Actions - Browser->>User: User Performs Optional Actions - User->>Browser: User Optional Actions Complete - Browser->>Partner: User Optional Actions Complete - Partner->>Browser: Redirect to Supabase Authorization Page + Browser->>Partner: Visit Redirect URL + Partner->>Partner: Validate Redirect Record + Partner->>Browser: Optional User Actions + Browser->>User: User Performs Optional Actions + User->>Browser: User Optional Actions Complete + Browser->>Partner: User Optional Actions Complete + Partner->>Browser: Redirect to Supabase Authorization Page - Browser->>Supabase: Request Authorization - Supabase->>Browser: Redirect to Consent Screen + Browser->>Supabase: Request Authorization + Supabase->>Browser: Redirect to Consent Screen - Browser->>User: Display Consent Screen - User->>Browser: Gives Consent + Browser->>User: Display Consent Screen + User->>Browser: Gives Consent - Browser->>Supabase: Submit User Consent - Supabase->>Browser: Redirect to Partner With Authorization Code + Browser->>Supabase: Submit User Consent + Supabase->>Browser: Redirect to Partner With Authorization Code - Browser->>Partner: Submit Authorization Code + Browser->>Partner: Submit Authorization Code - Partner->>Supabase: Exchange Authorization Code for Token - Supabase->>Partner: Return Token + Partner->>Supabase: Exchange Authorization Code for Token + Supabase->>Partner: Return Token - Partner->>Supabase: Request Management API Resources - Supabase->>Partner: Return Resources + Partner->>Supabase: Request Management API Resources + Supabase->>Partner: Return Resources - Partner->>Browser: Ok 200, OAuth Flow Complete + Partner->>Browser: Ok 200, OAuth Flow Complete - Browser->>User: Display "Integration Installed" Message`} - -/> + Browser->>User: Display "Integration Installed" Message +``` ### Step 1: Exchange public keys with Supabase diff --git a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx index e1204f22222..45be844a97f 100644 --- a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx +++ b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx @@ -160,7 +160,7 @@ curl 'https://api.supabase.com/v1/projects/{ref}/branches' \ --data '{ "branch_name": "DEV", "secrets": { - "STRIPE_SECRET_KEY":"sk_test_123..." + "STRIPE_SECRET_KEY":"sk_test_123...", "STRIPE_PUBLISHABLE_KEY":"pk_test_123..." } }' @@ -267,7 +267,7 @@ It's important that the data in development branches is NOT production data, esp -It's common in `DEV` branches to "seed" data. This is basically test data for users. Let's insert the following seed to our new todos table: +In `DEV` branches, seed data is common test data for users. Insert the following seed into the new todos table: ```sql insert into todos (task) diff --git a/apps/docs/content/guides/integrations/vercel-marketplace.mdx b/apps/docs/content/guides/integrations/vercel-marketplace.mdx index 8471fd1cdd4..35c554d1695 100644 --- a/apps/docs/content/guides/integrations/vercel-marketplace.mdx +++ b/apps/docs/content/guides/integrations/vercel-marketplace.mdx @@ -58,7 +58,7 @@ These variables ensure your applications can connect securely to the database an ## Studio support -Accessing Supabase Studio is simple through the Vercel dashboard. You can open Supabase Studio from either the Integration installation page or the Vercel Storage page. +Open Supabase Studio from the Vercel dashboard. You can access it from either the Integration installation page or the Vercel Storage page. Depending on your entry point, you'll either land on the Supabase dashboard homepage or be redirected to the corresponding Supabase Project. Supabase Studio provides tools such as: 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 8b4fb90c14d..8b531f49e4b 100644 --- a/apps/docs/content/guides/local-development/cli/getting-started.mdx +++ b/apps/docs/content/guides/local-development/cli/getting-started.mdx @@ -72,28 +72,12 @@ The Supabase CLI requires **Node.js 20 or later** when run via `npx` or `npm`. O - - -Installing the Supabase CLI globally using `npm install -g supabase` is **not supported**. - -For global usage, install the CLI via Homebrew, Scoop, or the standalone binary. - -Alternatively, you can run the CLI using `npx supabase` or install it locally as a dev dependency. - - - You can also install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase): ```sh npm install supabase --save-dev ``` - - -Global installation using `npm install -g supabase` is not supported. For global CLI usage, install via [Homebrew](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=macos), [Scoop](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=windows), or the [standalone binary](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=linux). - - - @@ -157,7 +141,7 @@ npx supabase@beta --help ## Updating the Supabase CLI -When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same methods. +When a new [version](https://github.com/supabase/cli/releases) is released, you can update the CLI using the same channels. -<$Show if="!docs:hide_cli_profiles"> - -<$Partial path="cli_profiles.mdx" /> - - - ## Running Supabase locally -The Supabase CLI uses Docker containers to manage the local development stack. Follow the official guide to install and configure [Docker Desktop](https://docs.docker.com/desktop): - - - - -Docker settings on Mac: Select Integrated, Virtualization Framework, and osxfs - - - - -Docker settings on Windows: Select Integrated, Expose Daemon, WSL2, and Add to /etc/hosts file. - - - - - +The Supabase CLI uses Docker containers to manage the local development stack. Follow the official guide to install and configure [Docker Desktop](https://docs.docker.com/desktop) on your machine. Alternately, you can use a different container tool that offers Docker compatible APIs. @@ -302,15 +241,13 @@ Alternately, you can use a different container tool that offers Docker compatibl - [OrbStack](https://orbstack.dev/) (macOS) - [colima](https://github.com/abiosoft/colima) (macOS) - - Inside the folder where you want to create your project, run: ```bash supabase init ``` -This will create a new `supabase` folder. It's safe to commit this folder to your version control system. +This creates a new `supabase` folder. It's safe to commit this folder to version control. Now, to start the Supabase stack, run: @@ -318,11 +255,11 @@ Now, to start the Supabase stack, run: supabase start ``` -This takes time on your first run because the CLI needs to download the Docker images to your local machine. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development (like a local SMTP server and a database diff tool). +This takes time on your first run because the CLI needs to download the Docker images to your local machine. The CLI includes the entire Supabase stack, and a few additional images useful for local development (like a local SMTP server and a database diff tool). ## Access your project's services -Once all of the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like this, with urls and keys that you'll use in your local project: +Once all the Supabase services are running, you'll see output containing your local Supabase credentials. It should look like the below, with urls and keys that you use in your local project: ``` Started supabase local development setup. @@ -433,7 +370,7 @@ For advanced logs analysis using the Logs Explorer, it is advised to use the Big -All logs will be stored in the local database under the `_analytics` schema. +All logs are stored in the local database under the `_analytics` schema. diff --git a/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx b/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx index 5246f956484..2fc83fe1ee9 100644 --- a/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx +++ b/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx @@ -5,7 +5,7 @@ description: 'Using the CLI to test your Supabase project.' subtitle: 'Using the CLI to test your Supabase project.' --- -The Supabase CLI provides a set of tools to help you test and lint your Postgres database and Edge` Functions. +The Supabase CLI provides a set of tools to help you test and lint your Postgres database and Edge Functions. ## Testing your database @@ -44,7 +44,7 @@ By default, Mailpit is available at [localhost:54324](http://localhost:54324) wh ### Going into production -The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, you must configure your own email provider. This is as simple as enabling a new SMTP credentials in your [project settings](/dashboard/project/_/auth/smtp). +The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, configure your own email provider by enabling SMTP credentials in your [project settings](/dashboard/project/_/auth/smtp). ## Linting your database diff --git a/apps/docs/content/guides/local-development/testing/overview.mdx b/apps/docs/content/guides/local-development/testing/overview.mdx index 8c4624a92a6..9b0e3ac7e09 100644 --- a/apps/docs/content/guides/local-development/testing/overview.mdx +++ b/apps/docs/content/guides/local-development/testing/overview.mdx @@ -17,12 +17,12 @@ Testing is a critical part of database development, especially when working with - Functions and procedures - Data integrity -This example demonstrates setting up and testing RLS policies for a simple todo application: +This example demonstrates setting up and testing RLS policies for a basic todo application: 1. Create a test table with RLS enabled: ```sql - -- Create a simple todos table + -- Create a todos table create table todos ( id uuid primary key default gen_random_uuid(), task text not null, diff --git a/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx b/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx index b7bbefecbea..3922893cc9e 100644 --- a/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx +++ b/apps/docs/content/guides/local-development/testing/pgtap-extended.mdx @@ -83,7 +83,7 @@ The test helpers package provides several advantages over writing raw pgTAP test ## Schema-wide Row Level Security testing -When working with Row Level Security, it's crucial to ensure that RLS is enabled on all tables that need it. Create a simple test to verify RLS is enabled across an entire schema: +When working with Row Level Security, it's crucial to ensure that RLS is enabled on all tables that need it. Create a basic test to verify RLS is enabled across an entire schema: ```sql begin; @@ -112,7 +112,7 @@ This setup file should contain: 1. All shared extensions and dependencies 2. Common test utilities -3. A simple always green test to verify the setup +3. A basic always-green test to verify the setup Here's an example setup file: @@ -494,7 +494,7 @@ create policy "Users can update their own comments" #### 4. Test cases: -Now everything is setup, let's write RLS test cases, note that each section could be in its own test: +With setup complete, write RLS test cases. Each section can be in its own test: ```sql -- Assuming we already have: 000-setup-tests-hooks.sql file we can use tests helpers diff --git a/apps/docs/content/guides/platform.mdx b/apps/docs/content/guides/platform.mdx index f4c58fe84a0..3953d8a1862 100644 --- a/apps/docs/content/guides/platform.mdx +++ b/apps/docs/content/guides/platform.mdx @@ -5,7 +5,7 @@ description: 'Getting started with the Supabase Platform.' sidebar_label: 'Overview' --- -Supabase is a hosted platform which makes it very simple to get started without needing to manage any infrastructure. +Supabase is a hosted platform that allows you to get started without needing to manage any infrastructure. Visit [supabase.com/dashboard](/dashboard) and sign in to start creating projects. diff --git a/apps/docs/content/guides/platform/access-control.mdx b/apps/docs/content/guides/platform/access-control.mdx index e3490f0d70c..73bea005159 100644 --- a/apps/docs/content/guides/platform/access-control.mdx +++ b/apps/docs/content/guides/platform/access-control.mdx @@ -264,7 +264,7 @@ The table below shows the actions each role can take on the resources belonging | Production Branch | Read | | | | | | | Write | | | | | | Development Branches | List | | | | | -| | Create | | | | | +| | Create[^8] | | | | | | | Update | | | | | | | Delete | | | | | @@ -281,3 +281,5 @@ The table below shows the actions each role can take on the resources belonging [^6]: Listed permissions are for the API and Dashboard. [^7]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html). + +[^8]: When using dashboard branching without a GitHub integration, the first branch creation also registers the project's production branch — a one-time step that requires Owner or Administrator. See [Branching via the dashboard](/docs/guides/deployment/branching/dashboard) for details. Developers can create, update, and delete branches normally after that. diff --git a/apps/docs/content/guides/platform/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index 6d437b241f5..9a2123b6f4c 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -3,7 +3,7 @@ title: 'Database Backups' description: 'Learn about backups for your Supabase project.' --- -We automatically back up all Free, Pro, Team, and Enterprise Plan projects on a daily basis. You can find backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard. +We automatically back up all Pro, Team, and Enterprise Plan projects on a daily basis. You can find backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard. Pro Plan projects can access the last 7 days of daily backups. Team Plan projects can access the last 14 days of daily backups, while Enterprise Plan projects can access up to 30 days of daily backups. If you need more frequent backups, consider enabling [Point-in-Time Recovery](#point-in-time-recovery). We recommend that free tier plan projects regularly export their data using the [Supabase CLI `db dump` command](/docs/reference/cli/supabase-db-dump) and maintain off-site backups. diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 24d8cd58770..032103c704c 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -66,7 +66,7 @@ Read more about [Compute usage](/docs/guides/platform/manage-your-usage/compute) #### What is egress and how is it billed? -Egress refers to the total bandwidth (network traffic) quota available to each organization. This quota can be utilized for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly. +Egress refers to the total bandwidth (network traffic) quota available to each organization. This quota can be used for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly. We differentiate between cached (served via our CDN from cache hits) and uncached egress and give quotas for each type and have varying pricing (cached egress is cheaper). Cached egress only applies to Storage. diff --git a/apps/docs/content/guides/platform/cost-control.mdx b/apps/docs/content/guides/platform/cost-control.mdx index 95a247925d4..580e67fcb77 100644 --- a/apps/docs/content/guides/platform/cost-control.mdx +++ b/apps/docs/content/guides/platform/cost-control.mdx @@ -34,6 +34,8 @@ When the Spend Cap is off, we recommend monitoring your usage and costs on the [ - [Disk Size](/docs/guides/platform/manage-your-usage/disk-size) - [Egress](/docs/guides/platform/manage-your-usage/egress) - [Edge Function Invocations](/docs/guides/platform/manage-your-usage/edge-function-invocations) +- [Logs Ingest](/docs/guides/platform/manage-your-usage/logs-ingest) +- [Logs Query](/docs/guides/platform/manage-your-usage/logs-query) - [Monthly Active Users](/docs/guides/platform/manage-your-usage/monthly-active-users) - [Monthly Active SSO Users](/docs/guides/platform/manage-your-usage/monthly-active-users-sso) - [Monthly Active Third Party Users](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party) diff --git a/apps/docs/content/guides/platform/custom-domains.mdx b/apps/docs/content/guides/platform/custom-domains.mdx index 441a7657a4d..66f65eb63fb 100644 --- a/apps/docs/content/guides/platform/custom-domains.mdx +++ b/apps/docs/content/guides/platform/custom-domains.mdx @@ -90,6 +90,7 @@ Use the [`domains reverify`](/docs/reference/cli/supabase-domains-reverify) comm supabase domains reverify --project-ref abcdefghijklmnopqrst ``` +{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} In the background, Supabase will check your DNS records and issue an SSL certificate. Supabase uses multiple Certificate Authorities (including Let's Encrypt, Google Trust Services and SSL.com) to ensure high availability. The specific issuer is chosen based on availability and this process can take up to 30 minutes. ### Prepare to activate your domain @@ -160,7 +161,7 @@ To get started: You can configure vanity subdomains via the CLI only. -Let's assume your Supabase project's domain is `abcdefghijklmnopqrst.supabase.co` and you wish to configure a vanity subdomain at `my-example-brand.supabase.co`. +Assume your Supabase project's domain is `abcdefghijklmnopqrst.supabase.co` and you wish to configure a vanity subdomain at `my-example-brand.supabase.co`. ### Check subdomain availability diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index 9af02dff399..f2e63601fb7 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -26,7 +26,7 @@ Database size is consumed primarily by your data, indexes, and materialized view -Depending on your billing plan, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Disk Management](#disk-management) section. +Depending on your billing plan, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Read-Only Mode](#read-only-mode) section. diff --git a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx index 64e08ed131d..a3747e1aceb 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/edge-function-invocations.mdx @@ -13,7 +13,7 @@ Edge Function Invocations are billed using Package pricing, with each package re ### Example -For simplicity, let's assume a package size of 1 million and a charge of per package without a free quota. +For simplicity, assume a package size of 1 million and a charge of per package without a free quota. | Invocations | Packages Billed | Costs | | ----------- | --------------- | ------------------- | diff --git a/apps/docs/content/guides/platform/manage-your-usage/logs-ingest.mdx b/apps/docs/content/guides/platform/manage-your-usage/logs-ingest.mdx new file mode 100644 index 00000000000..af40d0e9678 --- /dev/null +++ b/apps/docs/content/guides/platform/manage-your-usage/logs-ingest.mdx @@ -0,0 +1,73 @@ +--- +id: 'manage-usage-logs-ingest' +title: 'Manage Logs Ingest usage' +--- + + + +Logs pricing is being rolled out. Quotas and rates on this page are confirmed but billing enforcement is not yet live. This page will be updated when the rollout is complete. + + + +## What you are charged for + +You are charged for the total volume of log data that Supabase ingests across all your project's services (Postgres, API gateway, Auth, Storage, Realtime, Edge Functions, etc.) during the billing cycle, measured in GB. + +## How charges are calculated + +Logs Ingest is charged per GB of log data ingested during the billing cycle. + +### Usage on your invoice + +Usage is shown as "Logs Ingest" on your invoice. + +## Pricing + +<$Partial path="billing/pricing/pricing_logs_ingest.mdx" /> + +## Billing examples + +### Within quota + +The organization's Logs Ingest usage is within the quota, so no charges for Logs Ingest apply. + +| Line Item | Units | Costs | +| ------------------- | --------- | ------------------------ | +| Pro Plan | 1 | | +| Compute Hours Micro | 744 hours | | +| Logs Ingest | 2 GB | | +| **Subtotal** | | **** | +| Compute Credits | | - | +| **Total** | | **** | + +### Exceeding quota + +The organization's Logs Ingest usage exceeds the quota by 7 GB, incurring charges for this additional usage. + +| Line Item | Units | Costs | +| ------------------- | --------- | -------------------------- | +| Pro Plan | 1 | | +| Compute Hours Micro | 744 hours | | +| Logs Ingest | 12 GB | | +| **Subtotal** | | **** | +| Compute Credits | | - | +| **Total** | | **** | + +## View usage + +You can view Logs Ingest usage on the [organization's usage page](/dashboard/org/_/usage) of the Dashboard. The page shows the usage of all projects by default. To view the usage for a specific project, select it from the dropdown. You can also select a different time period. + +{/* TODO: Add screenshots once Studio surfaces are live */} + +## Optimize usage + +Every service in your Supabase project automatically generates Logs — you don't write them directly. Log volume scales with your application's traffic and behavior. To reduce ingest volume: + +- **Reduce log-level verbosity** in your Edge Functions and server-side code (for example, `info` → `warn` in production). +- **Audit verbose logging in your application code.** Application-level logs forwarded to Supabase services count toward ingest. +- **Cap log payload size.** Large structured payloads inflate GB-billed volume quickly. +- **Investigate spikes.** Use the [**Logs Explorer**](/dashboard/project/_/logs-explorer) section of the Dashboard to find services or endpoints producing unusually high volume. + +## Exceeding Quotas + +<$Partial path="billing/exceeding_usage_quotas.mdx" /> diff --git a/apps/docs/content/guides/platform/manage-your-usage/logs-query.mdx b/apps/docs/content/guides/platform/manage-your-usage/logs-query.mdx new file mode 100644 index 00000000000..6983989b27f --- /dev/null +++ b/apps/docs/content/guides/platform/manage-your-usage/logs-query.mdx @@ -0,0 +1,70 @@ +--- +id: 'manage-usage-logs-query' +title: 'Manage Logs Query usage' +--- + + + +Logs pricing is being rolled out. Quotas and rates on this page are confirmed but billing enforcement is not yet live. This page will be updated when the rollout is complete. + + + +## What you are charged for + +You are charged for the volume of log data scanned when you read logs via the Studio UI, the Management API, the CLI, or any other interface, measured in GB. + +## How charges are calculated + +Logs Query is charged per GB of log data scanned during the billing cycle. + +### Usage on your invoice + +Usage is shown as "Logs Query" on your invoice. + +## Pricing + +<$Partial path="billing/pricing/pricing_logs_query.mdx" /> + +## Billing examples + +### Within quota + +The organization's Logs Query usage is within the quota, so no charges for Logs Query apply. + +| Line Item | Units | Costs | +| ------------------- | --------- | ------------------------ | +| Pro Plan | 1 | | +| Compute Hours Micro | 744 hours | | +| Logs Query | 300 GB | | +| **Subtotal** | | **** | +| Compute Credits | | - | +| **Total** | | **** | + +### Exceeding quota + +The organization's Logs Query usage exceeds the quota by 4,000 GB, incurring charges for this additional usage. + +| Line Item | Units | Costs | +| ------------------- | --------- | ------------------------ | +| Pro Plan | 1 | | +| Compute Hours Micro | 744 hours | | +| Logs Query | 5,000 GB | | +| **Subtotal** | | **** | +| Compute Credits | | - | +| **Total** | | **** | + +## View usage + +You can view Logs Query usage on the [organization's usage page](/dashboard/org/_/usage) of the Dashboard. The page shows the usage of all projects by default. To view the usage for a specific project, select it from the dropdown. You can also select a different time period. + +{/* TODO: Add screenshots once Studio surfaces are live */} + +## Optimize usage + +- **Narrow your query time ranges.** Querying a 7-day window scans 7× more data than a 1-day window. +- **Use filters early.** Adding service or endpoint filters reduces the volume scanned. +- **Be cautious with programmatic log access.** Polling the logs API frequently from scripts, agents, or third-party integrations can inflate query GB rapidly. If you need to stream logs continuously, [Log Drains](/docs/guides/platform/manage-your-usage/log-drains) are a more cost-effective option than repeated queries. + +## Exceeding Quotas + +<$Partial path="billing/exceeding_usage_quotas.mdx" /> diff --git a/apps/docs/content/guides/platform/manage-your-usage/logs.mdx b/apps/docs/content/guides/platform/manage-your-usage/logs.mdx new file mode 100644 index 00000000000..26a334c289e --- /dev/null +++ b/apps/docs/content/guides/platform/manage-your-usage/logs.mdx @@ -0,0 +1,32 @@ +--- +id: 'manage-usage-logs' +title: 'Manage Logs usage' +--- + + + +Logs pricing is being rolled out. Quotas and rates on this page are confirmed but billing enforcement is not yet live. This page will be updated when the rollout is complete. + + + +Logs usage is metered on two SKUs: + +- **Logs Ingest** — the total GB of log data Supabase ingests across all your project's services (Postgres, API gateway, Auth, Storage, Realtime, Edge Functions, etc.) during the billing cycle. +- **Logs Query** — the total GB of log data scanned when you read logs via the Studio UI, the Management API, the CLI, or any other interface. + +Each plan includes a free quota for both. Usage beyond the quota is billed per GB. + +| SKU | Included (per month) | Over-Usage (Pro, Team) | +| ----------- | -------------------- | ------------------------------ | +| Logs Ingest | 5 GB | per GB | +| Logs Query | 1,000 GB | per GB | +| Enterprise | Custom | Custom | + +For full billing details, invoice examples, and optimization tips, see the per-SKU pages: + +- [Manage Logs Ingest usage](/docs/guides/platform/manage-your-usage/logs-ingest) +- [Manage Logs Query usage](/docs/guides/platform/manage-your-usage/logs-query) + +## Logs vs log drains + +[Log Drains](/docs/guides/platform/manage-your-usage/log-drains) stream logs out of Supabase to external destinations (Datadog, Better Stack, your own S3 bucket, etc.) and are billed separately on drain hours and events. Draining logs does not replace or reduce Logs Ingest charges — ingest is metered when Supabase processes your logs, drains are metered when Supabase streams them out. These are separate billing primitives, not overlapping charges. diff --git a/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx b/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx index 5d582735396..ba6dc01a594 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/realtime-messages.mdx @@ -19,7 +19,7 @@ Realtime Messages are billed using Package pricing, with each package representi ### Example -For simplicity, let's assume a package size of 1,000,000 and a charge of per package without quota. +For simplicity, assume a package size of 1,000,000 and a charge of per package without quota. | Messages | Packages Billed | Costs | | --------- | --------------- | ---------------------- | diff --git a/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx b/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx index 8722b1cd36a..b8fa985060c 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/realtime-peak-connections.mdx @@ -24,7 +24,7 @@ Realtime Peak Connections are billed using Package pricing, with each package re ### Example -For simplicity, let's assume a package size of 1,000 and a charge of per package with no quota. +For simplicity, assume a package size of 1,000 and a charge of per package with no quota. | Peak Connections | Packages Billed | Costs | | ---------------- | --------------- | -------------------- | diff --git a/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx index f09f5861aed..743ef928393 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-image-transformations.mdx @@ -53,7 +53,7 @@ Storage Image Transformations are billed using Package pricing, with each packag ### Example -For simplicity, let's assume a package size of 1,000 and a charge of per package with no quota. +For simplicity, assume a package size of 1,000 and a charge of per package with no quota. | Origin Images | Packages Billed | Costs | | ------------- | --------------- | -------------------- | diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx index 8bcf027f606..c686e8ca188 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/backup-restore.mdx @@ -4,9 +4,9 @@ subtitle: 'Learn how to backup and restore projects using the Supabase CLI' breadcrumb: 'Migrations' --- -# Migrating the database +## Migrating the database -## Backup database using the CLI +### Back up database using the CLI @@ -74,7 +74,7 @@ breadcrumb: 'Migrations' -## Before you begin +### Before you begin -## Restore backup using CLI +### Restore backup using CLI @@ -197,9 +197,9 @@ breadcrumb: 'Migrations' -## Special considerations +### Special considerations -#### Preserving migration history +##### Preserving migration history If you were using Supabase CLI for managing migrations on your old database and would like to preserve the migration history in your newly restored project, you need to insert the migration records separately using the following commands. @@ -214,7 +214,7 @@ psql \ --dbname "$NEW_DB_URL" ``` -#### Schema changes to `auth` and `storage` +##### Schema changes to `auth` and `storage` If you have modified the `auth` and `storage` schemas in your old project, such as adding triggers or Row Level Security(RLS) policies, you have to restore them separately. The Supabase CLI can help you diff the changes to these schemas using the following commands. @@ -223,13 +223,13 @@ supabase link --project-ref "$OLD_PROJECT_REF" supabase db diff --linked --schema auth,storage > changes.sql ``` -## Troubleshooting notes +### Troubleshooting notes -#### Disabling triggers during restore: +##### Disabling triggers during restore: Setting `session_replication_role` to `replica` disables triggers during the migration, preventing columns from being double encrypted. -#### Custom roles require passwords +##### Custom roles require passwords If you created any [custom roles](/dashboard/project/_/database/roles) with the `LOGIN` attribute, you must manually set their passwords in the new project. This can be done with the SQL command: @@ -237,7 +237,7 @@ If you created any [custom roles](/dashboard/project/_/database/roles) with the alter user "YOUR_USER" with password 'SOME_NEW_PASSWORD'; ``` -#### `supabase_admin` permission errors +##### `supabase_admin` permission errors If you encounter permission errors related to `supabase_admin` during restore: @@ -248,7 +248,7 @@ If you encounter permission errors related to `supabase_admin` during restore: ALTER ... OWNER TO "supabase_admin" ``` -#### `cli_login_postgres` role grant error +##### `cli_login_postgres` role grant error If you encounter the error: @@ -264,7 +264,7 @@ DETAIL: Only roles with the ADMIN option on role "postgres" may grant this role GRANT "postgres" TO "cli_login_postgres" WITH INHERIT FALSE GRANTED BY "supabase_admin"; ``` -#### `cli_login_postgres` role issues after cloning +##### `cli_login_postgres` role issues after cloning The `cli_login_role` must be created by the `supabase_admin` role. If the migration process cloned over the role before the CLI could generate its own version, it may encounter the error: @@ -279,9 +279,9 @@ To resolve the issue, drop the custom `cli_login_postgres` role. Then the CLI ca DROP ROLE IF EXISTS cli_login_postgres; ``` -# Migrating edge functions +## Migrating edge functions -## Steps (using the Supabase CLI): +### Steps (using the Supabase CLI): @@ -326,7 +326,7 @@ DROP ROLE IF EXISTS cli_login_postgres; -## Steps (using the Supabase Dashboard): +### Steps (using the Supabase Dashboard): @@ -369,7 +369,7 @@ Dependencies defined through [import maps](/docs/guides/functions/dependencies#u -# Migrating storage objects +## Migrating storage objects @@ -808,6 +808,6 @@ Dependencies defined through [import maps](/docs/guides/functions/dependencies#u -## Resources +### Resources - [Connecting with PSQL](/docs/guides/database/psql) diff --git a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx index 476c4a9aa87..3e59cdeb9a7 100644 --- a/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx +++ b/apps/docs/content/guides/platform/migrating-within-supabase/dashboard-restore.mdx @@ -6,6 +6,12 @@ breadcrumb: 'Migrations' ## Before you begin + + +Dashboard backups are only available for older projects that still use logical backups. Projects that use physical backups should follow steps in [Backup and Restore using the CLI](/docs/guides/platform/migrating-within-supabase/backup-restore). + + + -If you are already ingesting your [project's metrics](/docs/guides/platform/metrics#accessing-the-metrics-endpoint) into your own environment, you can also keep track of replication lag and set alarms with the `physical_replication_lag_physical_replica_lag_seconds` metric. +If you are already ingesting your [project's metrics](/docs/guides/telemetry/metrics) into your own environment, you can also keep track of replication lag and set alarms with the `physical_replication_lag_physical_replica_lag_seconds` metric. @@ -138,7 +138,7 @@ If you are already ingesting your [project's metrics](/docs/guides/platform/metr Some common sources of high replication lag include: 1. **Exclusive locks on tables on the Primary**: Operations such as `drop table` and `reindex` take an access-exclusive lock on the table. This can result in increasing replication lag for the duration of the lock. -2. **Resource Constraints on the database**: Heavy utilization on the primary or the replica, if run on an under-resourced project, can result in high replication lag. This includes the characteristics of the disk being utilized (IOPS, Throughput). +2. **Resource Constraints on the database**: Heavy utilization on the primary or the replica, if run on an under-resourced project, can result in high replication lag. This includes the characteristics of the disk being used (IOPS, Throughput). 3. **Long-running transactions on the Primary**: Transactions that run for a long-time on the primary can also result in high replication lag. You can use the `pg_stat_activity` view to identify and terminate such transactions if needed. `pg_stat_activity` is a live view, and does not offer historical data on transactions that might have been active for a long time in the past. High replication lag can result in stale data returned for queries executed against the affected read replicas. diff --git a/apps/docs/content/guides/platform/temporary-access.mdx b/apps/docs/content/guides/platform/temporary-access.mdx new file mode 100644 index 00000000000..32e067c380c --- /dev/null +++ b/apps/docs/content/guides/platform/temporary-access.mdx @@ -0,0 +1,118 @@ +--- +title: 'Temporary access' +description: 'Enable temporary access for short-lived database connections tied to a Supabase user.' +--- + +Your Supabase project supports connecting to the Postgres database using either your Supabase API token (Personal Access Token) or your current dashboard session token (JWT). This is called temporary access, as the authentication tokens can be short-lived and tied directly to a specific Supabase user. Temporary access is disabled by default. + +Enabling temporary access only applies to connections to Postgres and Supavisor ("Connection Pooler"); all HTTP APIs offered by Supabase (e.g., PostgREST, Storage, Auth) require authentication tokens specific to the service and are independent of the Supabase platform user(s). + + + +Projects need to be at least on Postgres 17.6.1.081 (or higher) to enable temporary access. You can find the Postgres version of your project on the [infrastructure settings](/dashboard/project/_/settings/infrastructure) page. If your project is on an older version, you will need to [upgrade](/docs/guides/platform/upgrading) to use this feature. + + + +## Manage temporary access via the dashboard + +The easiest way to manage temporary access is via the "Enable temporary access" settings section in [Database Settings page](/dashboard/project/_/database/settings) of the dashboard. + +## Manage temporary access via the Management API + +You can also manage temporary access using the Management API: + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_MANAGEMENT_API_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Get current temporary access status +curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" + +# Enable temporary access +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "state":"enabled" + }' + +# Disable temporary access +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "state":"disabled" + }' +``` + +## Configure user access + +Once temporary access has been enabled, project users must be authorized and mapped to Postgres roles they are allowed to access. Each user can be authorized to "assume" one or more Postgres roles using temporary access. + +When a user is authorized to assume a Postgres role, the user's access token (Personal Access Token (PAT) or Scoped PAT) will be used as the password for the Postgres role. + + + +Postgres roles can still be accessed using the password configured on the role. This allows long-lived service connections to continue using the existing credentials. + +Temporary access authentication only applies for Supabase project users authenticating to the database, and assuming the given Postgres role. No new roles or users are created in the Postgres database and the assumed role's permissions will still apply. + + + +### Apply temporary access restrictions + +A user's temporary access can also be restricted to a validity period, after which their temporary access will expire and even though the access token is still valid, the database will reject the connection. + +IP address restrictions can also be applied, ensuring that temporary access will only be authorized from allowed network ranges (IPv4 and/or IPv6). + +#### Applying restrictions with the management API + +Restrictions can also be applied through the Management API. + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_MANAGEMENT_API_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Restrict temporary access to IPv4 ranges and expiry date +# user_id is the gotrue_id of the user with access to the project +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "00000000-1111-2222-3333-444444444444", + "user_roles": [ + { + "role": "postgres", + "allowed_networks": { + "allowed_cidrs": [{ "cidr": "176.1.12.1/32" }] + }, + "expires_at": 1758721065775 + } + ] + }' +``` + +## Using temporary access + +To log in to the database using temporary access, existing connection strings can be used and only the password needs to be changed to the user's API or dashboard token. + +For example, if a user has been authorized to assume the `postgres` role: + +``` +psql 'postgres://postgres:sbp_111222333aaabbbccc@db.{project-ref}.supabase.co/postgres' +``` + +Since Supabase API tokens can be used, it is also possible to generate API tokens for services you don't want to share your Postgres role password with (for example a GitHub Action). The API token can be configured with an expiry time and temporary access-specific restrictions can also be applied. + +Connecting via the shared connection pooler requires the addition of a new connection option. This can be applied either directly in the connection URI or as `conninfo` (easier to read): + +``` +# directly in the URI +psql 'postgres://postgres.{project-ref}:sbp_111222333aaabbbccc@aws-1-us-west-1.pooler.supabase.com:5432/postgres?options=-c%20jit%3don' + +# or as a connection info string +psql "host=aws-1-us-west-1.pooler.supabase.com user=postgres.{project-ref} options='-c jit=on'" +``` diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 58a2c79a6e1..25d87e3ebc7 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -2,57 +2,42 @@ title: Upgrading --- -Supabase ships fast and we endeavor to add all new features to existing projects wherever possible. In some cases, access to new features require upgrading or migrating your Supabase project. +Supabase ships fast and we try to add all new features to existing projects wherever possible. In some cases, access to new features require upgrading or migrating your Supabase project. It is recommended to upgrade Postgres version to get access to the latest features and fixes. - +For scaling your compute size, refer to the [Compute and Disk page](/docs/guides/platform/compute-and-disk). -This guide refers to upgrading the Postgres version of your Supabase Project. For scaling your compute size, refer to the [Compute and Disk page](/docs/guides/platform/compute-and-disk). +## How we upgrade - - -You can upgrade your project using in-place upgrades or by pausing and restoring your project. - -## In-place upgrades +The process remains the same for Postgres major and minor version upgrades, as other features and services are also upgraded at the same time. -For security purposes, passwords for custom roles are not backed up and, following a restore, they would need to be reset. See [here](/docs/guides/platform/backups#daily-backups) for more details +Free projects will move to the latest minor version when their paused project is restored. Paid projects can't be paused. -In-place upgrades uses `pg_upgrade`. For projects larger than 1GB, this method is generally faster than a pause and restore cycle, and the speed advantage grows with the size of the database. +The upgrade process is as follows: -1. Plan for an appropriate downtime window, and ensure you have reviewed the [caveats](#caveats) section of this document before executing the upgrade. 1. Use the "Upgrade project" button on the [Infrastructure](/dashboard/project/_/settings/infrastructure) section of your dashboard. +2. An estimate of the time to upgrade is shown and anything that needs to be addressed before you are eligible to upgrade is shown as a warning. Ensure you have reviewed the [caveats](#caveats) section of this document before executing the upgrade. +3. Your project is taken offline and the Dashboard shows the upgrade status. +4. Behind the scenes, a new instance is created running the latest version of Supabase. +5. Your data is copied to the new instance and upgraded using `pg_upgrade`. +6. If the upgrade should fail, your original database would be brought back up online and be able to service requests. +7. When the upgrade succeeds, a [pg_basebackup](https://www.postgresql.org/docs/current/app-pgbasebackup.html) is taken and, once complete, your project is available in the Dashboard. -Additionally, if the upgrade should fail, your original database would be brought back up online and be able to service requests. +A Supabase project is deployed with a GP3 disk type by default, which will give ~100Mbps when upgrading. Changing the [disk type (or increasing IOPS/Throughput)](/docs/guides/platform/compute-and-disk) will reduce the time to upgrade. -As a rough rule of thumb, pg_upgrade operates at ~100MBps (when executing an upgrade on your data). Using the size of your database, you can use this metric to derive an approximate sense of the downtime window necessary for the upgrade. During this window, you should plan for your database and associated services to be unavailable. +Using the size of your database, you can use this metric to derive an approximation of the downtime window necessary for the upgrade. During this window, you should plan for your database and associated services to be unavailable. -## Pause and restore +## Upgrade pre-requisites - +When upgrading, a notification will inform you about what is blocking the upgrade process. You need to follow the pre-requisites for the upgrade to successfully complete: -We recommend using the In-place upgrade method, as it is faster, and more reliable. Additionally, only Free-tier projects are eligible to use the Pause and Restore method. - - - -When you pause and restore a project, the restored database includes the latest features. **This method includes downtime**, so be aware that your project will be inaccessible for a short period of time. - -1. On the [General Settings](/dashboard/project/_/settings/general) page in the Dashboard, click **Pause project**. You will be redirected to the home screen in the meantime. -1. After this, click **Restore project**. Your project will be restored from the [physical backup](/guides/platform/backups). You should receive an email once the restoration is complete. - -Pausing and restoring project will take some time depending on how much data your database has. If the restore process fails, [contact Supabase support](/dashboard/support/new?projectRef=) to bring your project back online. - -## Caveats - -Regardless of the upgrade method, a few caveats apply: - -### Logical replication - -If you are using logical replication, the replication slots will not be preserved by the upgrade process. You will need to manually recreate them after the upgrade with the method `pg_create_logical_replication_slot`. Refer to the Postgres docs on [Replication Management Functions](https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-REPLICATION) for more details about the method. - -### Breaking changes +1. Projects with read-replicas can't be upgraded. You need to delete the replicas and re-create them after upgrade completes. +2. `pg_upgrade` does not support upgrading of databases containing `reg*` data types referencing system OIDs. You need to modify the data to not use `reg*` data types before upgrade. +3. Logical replication slots must be dropped. +4. Deprecated/unsupported extensions must be dropped. Extensions can have dependencies, make sure you backup that data to restore it after the upgrade, with the updated extension version. Newer versions of services can break functionality or change the performance characteristics you rely on. If your project is eligible for an upgrade, you will be able to find your current service versions from within [the Supabase dashboard](/dashboard/project/_/settings/infrastructure). @@ -63,70 +48,22 @@ Breaking changes are generally only present in major version upgrades of Postgre If you are upgrading from a significantly older version, you will need to consider the release notes for any intermediary releases as well. -### Time limits +## Pre-upgrade best practices -Starting from 2024-06-24, when a project is paused, users then have a 90-day window to restore the project on the platform from within Supabase Studio. +1. Make sure to discuss with your teams a suitable maintenance window as upgrading involves downtime, this will be crucial for minimising the impact. +2. For smaller databases, we recommend taking a logical backup of the data using [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) utility. This is to ensure you have sufficient backup before upgrading. +3. For larger databases, ensure that a recent backup is in the [Backups](/dashboard/project/_/database/backups/scheduled) page in the Dashboard. +4. Reduce the size of the data and the number of objects in the database. The time to upgrade highly depends on these factors, and can influence downtime. For example: Archiving data which is not actively used, dropping unused indexes, running vacuum etc. See the [Inspect](/docs/reference/cli/supabase-inspect-db) command in the Supabase CLI for a detailed report on space that can be gained, as well as the [pg_repack](/docs/guides/database/extensions/pg_repack) documentation. -The 90-day window allows Supabase to introduce platform changes that may not be backwards compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes. +## Post-upgrade best practices -During the 90-day restore window a paused project can be restored to the platform with a single button click from [Studio's dashboard page](/dashboard/projects). +1. Supabase performs extensive pre- and post-upgrade validations to ensure that the database has been correctly upgraded. However, you should plan for your own application-level validations, as there might be changes you might not have anticipated, and this should be budgeted for when planning your downtime window. +2. Analyze logs for any new slow-running queries that may have emerged post-upgrade. This is possible due to the change in data structure during upgrade. +3. Verify extension versions, and look for the extensions you need to upgrade. - - -After the 90-day restore window, you can download your project's backup file, and Storage objects from the project dashboard. You can restore the data in the following ways: - -- [Restore a backup to a new Supabase project](/docs/guides/platform/migrating-within-supabase/dashboard-restore) -- [Restore a backup locally](/docs/guides/local-development/restoring-downloaded-backup) - - - -If you upgrade to a paid plan while your project is paused within the 90-day restore window, any expired one-click restore options are reenabled. Since the backup was taken outside the backwards compatibility window, it may fail to restore. If you have a problem restoring your backup after upgrading, contact [Support](/support). - - - -### Disk sizing - -When upgrading, the Supabase platform will "right-size" your disk based on the current size of the database. For example, if your database is 100GB in size, and you have a 200GB disk, the upgrade will reduce the disk size to 120GB (1.2x the size of your database). - -### Objects dependent on Postgres extensions - -In-place upgrades do not support upgrading of databases containing reg\* data types referencing system OIDs. -If you have created any objects that depend on the following extensions, you will need to recreate them after the upgrade. - -### `pg_cron` records - -[pg_cron](https://github.com/citusdata/pg_cron#viewing-job-run-details) does not automatically clean up historical records. This can lead to extremely large `cron.job_run_details` tables if the records are not regularly pruned; you should clean unnecessary records from this table prior to an upgrade. - -During an in-place upgrade, the `pg_cron` extension gets dropped and recreated. Prior to this process, the `cron.job_run_details` table is duplicated to avoid losing historical logs. The instantaneous disk pressure created by duplicating an extremely large details table can cause at best unnecessary performance degradation, or at worst, upgrade process failures. - -### Extensions - -In-place upgrades do not currently support upgrading of databases using extensions older than the following versions: - -- TimescaleDB 2.16.1 -- plv8 3.1.10 - -To upgrade to a newer version of Postgres, you will need to drop the extensions before the upgrade, and recreate them after the upgrade. - -#### Authentication method changes - deprecating md5 in favor of scram-sha-256 +### Custom roles with md5 passwords The md5 hashing method has [known weaknesses](https://en.wikipedia.org/wiki/MD5#Security) that make it unsuitable for cryptography. As such, we are deprecating md5 in favor of [scram-sha-256](https://www.postgresql.org/docs/current/auth-password.html), which is the default and most secure authentication method used in the latest Postgres versions. @@ -151,9 +88,45 @@ ALTER ROLE WITH PASSWORD ''; As part of the upgrade process, maintenance operations such as [vacuuming](https://www.postgresql.org/docs/current/routine-vacuuming.html#ROUTINE-VACUUMING) are also executed. This can result in a reduction in the reported database size. -### Post-upgrade validation +### Disk sizing -Supabase performs extensive pre- and post-upgrade validations to ensure that the database has been correctly upgraded. However, you should plan for your own application-level validations, as there might be changes you might not have anticipated, and this should be budgeted for when planning your downtime window. +When upgrading, the Supabase platform will "right-size" your disk based on the current size of the database. For example, if your database is 100GB in size, and you have a 200GB disk, the upgrade will reduce the disk size to 120GB (1.2x the size of your database). + +### Time limits + +Starting from 2024-06-24, when a project is paused, users then have a 90-day window to restore the project on the platform from within Supabase Studio. + +The 90-day window allows Supabase to introduce platform changes that may not be backwards compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes. + +During the 90-day restore window a paused project can be restored to the platform with a single button click from [Studio's dashboard page](/dashboard/projects). + +Project Paused: 90 Days Remaining + +After the 90-day restore window, you can download your project's backup file, and Storage objects from the project dashboard. You can restore the data in the following ways: + +- [Restore a backup to a new Supabase project](/docs/guides/platform/migrating-within-supabase/dashboard-restore) +- [Restore a backup locally](/docs/guides/local-development/restoring-downloaded-backup) + +Project Paused: Download Backup + +If you upgrade to a paid plan while your project is paused within the 90-day restore window, any expired one-click restore options are reenabled. Since the backup was taken outside the backwards compatibility window, it may fail to restore. If you have a problem restoring your backup after upgrading, contact [Support](/support). + +Project Paused: Paid Tier Restore ## Specific upgrade notes @@ -169,10 +142,20 @@ In projects using Postgres 17, the following extensions are deprecated: Projects planning to upgrade from Postgres 15 to Postgres 17 need to first disable these extensions in the [Supabase Dashboard](/dashboard/project/_/database/extensions). -`pgjwt` was enabled by default on every Supabase project up until Postgres 17. If you weren’t explicitly using `pgjwt` in your project, it’s most likely safe to disable. + + +`pgjwt` was enabled by default on every Supabase project up until Postgres 17. If you weren't explicitly using `pgjwt` in your project, it's most likely safe to disable. + + Existing projects on lower versions of Postgres are not impacted, and the extensions will continue to be supported on projects using Postgres 15, until the end of life of Postgres 15 on the Supabase platform. +### `pg_cron` usage + +[pg_cron](https://github.com/citusdata/pg_cron#viewing-job-run-details) does not automatically clean up historical records. This can lead to extremely large `cron.job_run_details` tables if the records are not regularly pruned; you should clean unnecessary records from this table before an upgrade. + +During the Supabase project upgrade, the `pg_cron` extension gets dropped and recreated. Before this process, the `cron.job_run_details` table is duplicated to avoid losing historical logs. The instantaneous disk pressure created by duplicating an extremely large details table can cause at best unnecessary performance degradation, or at worst, upgrade process failures. + ### Upgrading to pg_graphql 1.6.0 Starting with pg_graphql 1.6.0, GraphQL introspection is disabled by default. After the upgrade, queries to `__schema` and `__type` will return an error unless introspection is explicitly enabled. See the [pg_graphql configuration docs](https://supabase.github.io/pg_graphql/configuration/#introspection) for full details. diff --git a/apps/docs/content/guides/queues/quickstart.mdx b/apps/docs/content/guides/queues/quickstart.mdx index 58af639d01c..ce44b7fda84 100644 --- a/apps/docs/content/guides/queues/quickstart.mdx +++ b/apps/docs/content/guides/queues/quickstart.mdx @@ -3,7 +3,6 @@ title: Quickstart subtitle: 'Learn how to use Supabase Queues to add and read messages' --- -{/* */} 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. ## Concepts @@ -25,8 +24,6 @@ Supabase Queues offers three types of Queues: - **Basic Queue**: A durable Queue that stores Messages in a logged table. - **Unlogged Queue**: A transient Queue that stores Messages in an unlogged table for better performance but may result in loss of Queue Messages. -- **Partitioned Queue** (_Coming Soon_): A durable and scalable Queue that stores Messages in multiple table partitions for better performance. - ## Create Queues To get started, navigate to the [Supabase Queues](/dashboard/project/_/integrations/queues/overview) Postgres Module under Integrations in the Dashboard and enable the `pgmq` extension. @@ -40,8 +37,8 @@ To get started, navigate to the [Supabase Queues](/dashboard/project/_/integrati Supabase Dashboard Integrations page, showing the Queues Postgres Module - -If you've already created a Queue click the **Create a queue** button instead. - - +- Click **Create queue** button - Name your queue - + Queue names can only be lowercase and hyphens and underscores are permitted. - Select your [Queue Type](#queue-types) +- We recommend leaving Row Level Security (RLS) enabled. With it enabled, you don't need to set additional RLS on the queue tables. Create a Queue from the Supabase Dashboard -### What happens when you create a queue? + Every new Queue creates two tables in the `pgmq` schema. These tables are `pgmq.q_` to store and process active messages and `pgmq.a_` to store any archived messages. -A "Basic Queue" will create `pgmq.q_` and `pgmq.a_` tables as logged tables. +A "Basic Queue" creates `pgmq.q_` and `pgmq.a_` tables as logged tables. -However, an "Unlogged Queue" will create `pgmq.q_` as an unlogged table for better performance while sacrificing durability. The `pgmq.a_` table will still be created as a logged table so your archived messages remain safe and secure. +However, an "Unlogged Queue" creates `pgmq.q_` as an unlogged table for better performance while sacrificing durability. The `pgmq.a_` table is still created as a logged table so your archived messages remain safe and secure. + + ## Expose Queues to client-side consumers -Queues, by default, are not exposed over Supabase Data API and are only accessible via Postgres clients. +Queues, by default, are not exposed over the Supabase Data API and are only accessible via Postgres clients. However, you may grant client-side consumers access to your Queues by enabling the Supabase Data API and granting permissions to the Queues API, which is a collection of database functions in the `pgmq_public` schema that wraps the database functions in the `pgmq` schema. This is to prevent direct access to the `pgmq` schema and its tables (RLS is not enabled by default on any tables) and database functions. -To get started, navigate to the Queues [Settings page](/dashboard/project/_/integrations/queues/settings) and toggle on “Expose Queues via PostgREST”. Once enabled, Supabase creates and exposes a `pgmq_public` schema containing database function wrappers to a subset of `pgmq`'s database functions. +To get started, navigate to the [**Queues > Settings**](/dashboard/project/_/integrations/queues/settings) section of the Dashboard and enable **Expose Queues via PostgREST**. Once enabled, Supabase creates and exposes a `pgmq_public` schema containing database function wrappers to a subset of `pgmq`'s database functions. -Screenshot of Queues settings with toggle to expose to PostgREST +If you expose your pgmq schema with the Data API, for security purposes, you must enable Row Level Security (RLS) on all Queue tables (all tables in `pgmq` schema that begin with `q_`) -### Enable RLS on your tables in `pgmq` schema - -For security purposes, you must enable Row Level Security (RLS) on all Queue tables (all tables in `pgmq` schema that begin with `q_`) if the Data API is enabled. - -You’ll want to create RLS policies for any Queues you want your client-side consumers to interact with. - -Screenshot of creating an RLS policy from the Queues settings +Add an RLS policy for any Queues you want your client-side consumers to interact with, by clicking the _Add RLS Policy_ button on [the overview page of any Queue in the Dashboard](/dashboard/project/_/integrations/queues/queues). ### Grant permissions to `pgmq_public` database functions @@ -139,13 +111,13 @@ The permissions required for each Queue API database function: | `read` `pop` | `Select` `Update` | | `archive` `delete` | `Select` `Delete` | -To manage your queue permissions, click on the Queue Settings button. +To manage your queue permissions, click on the Queue Settings cog button on [the overview page of any Queue in the Dashboard](/dashboard/project/_/integrations/queues/queues). Screenshot of accessing queue settings + - - -`postgres` and `service_role` roles should never be exposed client-side. +You should never expose `postgres` and `service_role` roles client-side. ### Enqueueing and dequeueing messages -Once your Queue has been created, you can begin enqueueing and dequeueing Messages. +Once you have created your Queue, you can begin enqueueing and dequeueing Messages. - {[ - { - name: 'Multiplayer.dev', - description: 'Showcase application displaying cursor movements and chat messages using Broadcast.', - href: 'https://multiplayer.dev', - }, - { - name: 'Chat', - description: 'Supabase UI chat component using Broadcast to send message between users.', - href: 'https://supabase.com/ui/docs/nextjs/realtime-chat' - }, - { - name: 'Avatar Stack', - description: 'Supabase UI avatar stack component using Presence to track connected users.', - href: 'https://supabase.com/ui/docs/nextjs/realtime-avatar-stack' - }, - { - name: 'Realtime Cursor', - description: "Supabase UI realtime cursor component using Broadcast to share users' cursors to build collaborative applications.", - href: 'https://supabase.com/ui/docs/nextjs/realtime-cursor' - } -].map((x) => ( -
- - {x.description} +
+ + + Showcase application displaying cursor movements and chat messages using Broadcast. + + +
+
+ + + Supabase UI chat component using Broadcast to send message between users. + + +
+
+ + + Supabase UI avatar stack component using Presence to track connected users. + + +
+
+ + + Supabase UI realtime cursor component using Broadcast to share users' cursors to build + collaborative applications. +
-))} -
## Resources @@ -61,22 +61,19 @@ Check the [Getting Started](/docs/guides/realtime/getting_started) guide to get Find the source code and documentation in the Supabase GitHub repository.
- {[ - { - name: 'Supabase Realtime', - description: 'View the source code.', - href: 'https://github.com/supabase/realtime', - }, - { - name: 'Realtime: Multiplayer Edition', - description: 'Read more about Supabase Realtime.', - href: 'https://supabase.com/blog/supabase-realtime-multiplayer-general-availability', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + View the source code. + +
+
+ + + Read more about Supabase Realtime. + + +
diff --git a/apps/docs/content/guides/realtime/architecture.mdx b/apps/docs/content/guides/realtime/architecture.mdx index dab8ff0f051..cbf6b59737f 100644 --- a/apps/docs/content/guides/realtime/architecture.mdx +++ b/apps/docs/content/guides/realtime/architecture.mdx @@ -7,7 +7,7 @@ sidebar_label: 'Architecture' Realtime is a globally distributed Elixir cluster. Clients can connect to any node in the cluster via WebSockets and send messages to any other client connected to the cluster. -Realtime is written in [Elixir](https://elixir-lang.org/), which compiles to [Erlang](https://www.erlang.org/), and utilizes many tools the [Phoenix Framework](https://www.phoenixframework.org/) provides out of the box. +Realtime is written in [Elixir](https://elixir-lang.org/), which compiles to [Erlang](https://www.erlang.org/), and uses many tools the [Phoenix Framework](https://www.phoenixframework.org/) provides out of the box. Architecture - -Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys. - -**The legacy keys will be deprecated shortly, so we strongly encourage switching to and using the new publishable and secret API keys**. - -In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/): - -**For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. - -
+<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }} +/> + +Binary payloads (`ArrayBuffer` / `ArrayBufferView`) are received automatically from **supabase-js 2.91.0** and **supabase-swift 2.44.0**. On older SDK versions, binary messages are silently dropped and never reach the callback. + +
+ + +Broadcast payloads can be binary (`ArrayBuffer` or `ArrayBufferView`, e.g. `Uint8Array`) over WebSocket from **supabase-js 2.91.0** and **supabase-swift 2.44.0**. Binary payloads sent to clients running older SDK versions are **silently dropped** and never arrive over the WebSocket. The Dart, Kotlin, and Python clients don't support binary payloads yet. + + + @@ -344,6 +357,12 @@ You can use the Supabase client libraries to send Broadcast messages. <$Show if="sdk:swift"> + + + Binary payloads over WebSocket are supported from supabase-swift 2.44.0. Receivers on older SDK versions will not get binary messages. + + + {/* prettier-ignore */} ```swift let myChannel = await supabase.channel("test-channel") { @@ -394,7 +413,7 @@ You can use the Supabase client libraries to send Broadcast messages. - When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`. + When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, use `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`. @@ -449,11 +468,31 @@ By default, all database broadcasts are private, meaning clients must authentica +To broadcast a binary payload from your database, use the `realtime.send_binary()` function with a `bytea` payload: + +{/* prettier-ignore */} +```sql +select + realtime.send_binary( + '\x012345'::bytea, -- bytea payload + 'event', -- Event name + 'topic', -- Topic + true -- Private / Public flag (defaults to true) + ); +``` + +The same public/private matching rule applies: a binary broadcast only reaches channels with the same private setting. Binary messages only reach clients on **supabase-js 2.91.0** and **supabase-swift 2.44.0** or later; older clients silently drop them. + You can use the `realtime.broadcast_changes()` helper function to broadcast messages when a record is created, updated, or deleted. For more details, read [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes). ### Broadcast using the REST API -You can send a Broadcast message by making an HTTP request to Realtime servers. +You can send a single Broadcast message by making an HTTP request to Realtime servers. The endpoint embeds the topic and event in the path, and the `Content-Type` header determines the payload type: + +- `application/json` — JSON payload +- `application/octet-stream` — binary payload + +Add `?private=true` to broadcast to a private channel. ' \ -H 'Content-Type: application/json' \ - --data-raw '{ - "messages": [ - { - "topic": "test", - "event": "event", - "payload": { "test": "test" } - } - ] - }' \ - 'https://.supabase.co/realtime/v1/api/broadcast' + --data-raw '{ "test": "test" }' \ + 'https://.supabase.co/realtime/v1/api/broadcast/test/events/event' + + # Binary payload + curl -v \ + -H 'apikey: ' \ + -H 'Content-Type: application/octet-stream' \ + --data-binary @payload.bin \ + 'https://.supabase.co/realtime/v1/api/broadcast/test/events/event?private=true' ``` @@ -486,26 +525,39 @@ You can send a Broadcast message by making an HTTP request to Realtime servers. {/* prettier-ignore */} ```bash - POST /realtime/v1/api/broadcast HTTP/1.1 + POST /realtime/v1/api/broadcast/test/events/event HTTP/1.1 Host: {PROJECT_REF}.supabase.co Content-Type: application/json apikey: {SUPABASE_TOKEN} - { - "messages": [ - { - "topic": "test", - "event": "event", - "payload": { - "test": "test" - } - } - ] - } + { "test": "test" } ``` + + +To send multiple messages in a single request, the batch endpoint `POST /realtime/v1/api/broadcast` is still available. It accepts a JSON body with a `messages` array (JSON payloads only): + +{/* prettier-ignore */} +```bash +curl -v \ +-H 'apikey: ' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "messages": [ + { + "topic": "test", + "event": "event", + "payload": { "test": "test" } + } + ] +}' \ +'https://.supabase.co/realtime/v1/api/broadcast' +``` + + + ## Broadcast options You can pass configuration options while initializing the Supabase Client. @@ -637,7 +689,7 @@ You can pass configuration options while initializing the Supabase Client. - When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`. + When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, use `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`. @@ -792,7 +844,7 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv - This is currently available only in the Supabase JavaScript client version 2.37.0 and later. + `channel.httpSend()` always uses the REST API regardless of WebSocket connection state, and is available from the Supabase JavaScript client version 2.107.0 and later. `ArrayBuffer` and `ArrayBufferView` (e.g. `Uint8Array`) payloads are sent as `application/octet-stream`; all other payloads are JSON-encoded. @@ -801,13 +853,11 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv // No need to subscribe to channel - channel - .send({ - type: 'broadcast', - event: 'test', - payload: { message: 'Hi' }, - }) - .then((resp) => console.log(resp)) + // JSON payload + await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() }) + + // Binary payload (ArrayBuffer / ArrayBufferView) — sent as application/octet-stream + await channel.httpSend('cursor-pos', new Uint8Array([1, 2, 3]).buffer) // Remember to clean up the channel diff --git a/apps/docs/content/guides/realtime/limits.mdx b/apps/docs/content/guides/realtime/limits.mdx index edea5d6dc2d..5c45b111e4d 100644 --- a/apps/docs/content/guides/realtime/limits.mdx +++ b/apps/docs/content/guides/realtime/limits.mdx @@ -23,6 +23,7 @@ Upgrade your plan to increase your limits. Without a spend cap, or on an Enterpr | **Channels per connection** | 100 | 100 | 100 | 100 | 100+ | | **Presence keys per object** | 10 | 10 | 10 | 10 | 10+ | | **Presence messages per second** | 20 | 50 | 1,000 | 1,000 | 1,000+ | +| **Presence calls per client, per 30 seconds** | 5 | 5 | 5 | 5 | 5 | | **Broadcast payload size** | 256 KB | 3,000 KB | 3,000 KB | 3,000 KB | 3,000+ KB | | **Postgres change payload size ([**read more**](#postgres-changes-payload-limit))** | 1,024 KB | 1,024 KB | 1,024 KB | 1,024 KB | 1,024+ KB | diff --git a/apps/docs/content/guides/realtime/postgres-changes.mdx b/apps/docs/content/guides/realtime/postgres-changes.mdx index 9c5ac6caaeb..fc5673ed61b 100644 --- a/apps/docs/content/guides/realtime/postgres-changes.mdx +++ b/apps/docs/content/guides/realtime/postgres-changes.mdx @@ -4,7 +4,7 @@ subtitle: 'Listen to Postgres changes using Supabase Realtime.' description: 'Listen to Postgres changes using Supabase Realtime.' --- -Let's explore how to use Realtime's Postgres Changes feature to listen to database events. +Use Realtime's Postgres Changes to listen to database events. ## Quick start diff --git a/apps/docs/content/guides/realtime/presence.mdx b/apps/docs/content/guides/realtime/presence.mdx index a3403f5d6e3..5618cce8863 100644 --- a/apps/docs/content/guides/realtime/presence.mdx +++ b/apps/docs/content/guides/realtime/presence.mdx @@ -4,7 +4,7 @@ description: 'Share state between users with Realtime Presence.' subtitle: 'Share state between users with Realtime Presence.' --- -Let's explore how to implement Realtime Presence to track state between multiple users. +Use Realtime Presence to track state between multiple users. ## Usage @@ -47,17 +47,8 @@ The complete presence state returned by `presenceState()` looks like this: Get the Project URL and key from [the project's **Connect** dialog](/dashboard/project/_?showConnect=true). - - -Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys. - -**The legacy keys will be deprecated shortly, so we strongly encourage switching to and using the new publishable and secret API keys**. - -In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/_?showConnect=true), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/): - -**For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section. - - +<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }} +/> : "`. The server adds a backoff delay before replying, so avoid aggressive client-side retry loops on join errors. + +```json +{ + "status": "error", + "response": { "reason": "InvalidJWTExpiration: Token has expired 300 seconds ago" } +} +``` + +One exception: the `UnknownErrorOnChannel` code arrives as the bare human-readable string `"Unknown Error on Channel"` without the `: ` prefix. The JS client exposes the full `reason` string directly as the Error message without parsing it further. + +| Category | Error codes | Action | +| -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------- | +| Auth — expired token | `InvalidJWTExpiration` (message contains `"expired"`) | Refresh token, rejoin | +| Auth — invalid token | `MalformedJWT`, `JwtSignatureError`, `Unauthorized` | Do not retry; surface to caller | +| Rate limit | `ConnectionRateLimitReached`, `ClientJoinRateLimitReached`, `ChannelRateLimitReached` | Backoff, reduce join frequency | +| Database | `InitializingProjectConnection`, `IncreaseConnectionPool`, `DatabaseLackOfConnections`, `UnableToConnectToProject` | Retry with exponential backoff | +| Config | `TopicNameRequired`, `TenantNotFound`, `RealtimeDisabledForTenant`, `RealtimeDisabledForConfiguration` | Do not retry | +| Transient | `RealtimeRestarting` | Retry with backoff | + +### Channel-level system errors + +`extension: "system"`, `status: "error"`. Match on the `message` field content — there is no machine-readable code field. Every channel-level system error is immediately followed by `phx_close`; the channel is closed. Client libraries should expose a way for users to subscribe to `system` events since there is no automatic handling. + +| Message contains | Cause | Recovery | +| ------------------------------------------------- | -------------------------- | ----------------------------- | +| `Too many messages per second` | Broadcast/event rate limit | Throttle sends before rejoin | +| `Too many presence messages per second` | Tenant presence rate limit | Reduce presence frequency | +| `Client presence rate limit exceeded` | Per-client presence window | Longer cooldown before rejoin | +| `Track message size exceeded` | Presence payload too large | Shrink payload | +| `Token has expired` | JWT expired mid-session | Refresh token, rejoin | +| `Fields \`role\` and \`exp\` are required in JWT` | Claims missing | Fix token issuance | +| `Server requested disconnect` | Operational disconnect | Reconnect after delay | + +### Postgres Changes subscription errors + +`extension: "postgres_changes"`. These do **not** close the channel — broadcast and presence continue. `status: "ok"` with `message: "Subscribed to PostgreSQL"` confirms the subscription is live. + +| Scenario | Server retries? | Client action | +| ------------------------------------------------------ | ----------------- | -------------------------------------------------------------------------- | +| Invalid filter operator | No | Fix params and rejoin | +| Missing `schema`/`table` params | No | Fix params and rejoin | +| Subscription insert failed (table/publication missing) | Yes, every 5–10 s | Surface as degraded state; wait or check Realtime is enabled for the table | +| Database error during subscription | Yes, every 5–10 s | Surface as degraded state | +| `"Too many database timeouts"` | No | Reduce subscription load; retry later | + +Supported filter operators: `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`. + +The `ids` array on incoming `postgres_changes` payloads must match the subscription IDs returned in the `phx_join` reply. A mismatch means inconsistent server/client state — tear down and rejoin. + +### Broadcast errors + +Broadcast errors only affect **private channels**. When `config.broadcast.ack` is `false` (the default), all push failures — including size violations and RLS write denials — are silently dropped. RLS denials are always silent regardless of `ack`. + +When `ack` is `true`, the server replies on error with `response.error` (an atom string), not `response.reason`: + +```json +{ "status": "error", "response": { "error": "payload_size_exceeded" } } +``` + +Note that the JS client (`send()`) resolves to just the string `'error'` and does not expose the specific `error` atom to callers. + +### Presence errors + +Push replies surface payload-shape errors with `reason: "Presence track payload must be a map"`. Other push-level failures (RLS write denied, unknown event type, internal errors) return `status: "error"` with no reason field. + +Presence rate-limit and size violations arrive as channel-level system errors (see above) and close the channel. + +### Access token refresh + +Refresh the JWT in-band on private channels without rejoining using the `access_token` event: + +```json +["10", "1", "realtime:my-channel", "access_token", { "access_token": "" }] +``` + +There is no reply on success. On failure, the server emits a `system` error and closes the channel. Tokens with the `sb_*` prefix are silently ignored by the server. + +### Reconnection + +`phx_error` (unexpected server-side channel process termination, empty payload) should trigger a rejoin with exponential backoff. The JS client uses `[1000, 2000, 5000, 10000]` ms (capped at 10 s) configurable via `reconnectAfterMs`. + +`phx_close` following a rate-limit system error requires throttling before rejoin. Following a token system error, refresh the token first. A `phx_close` with no preceding system error is a clean close — only rejoin if it was unexpected. See [Limits](/docs/guides/realtime/limits) for the per-tenant thresholds that trigger rate-limit errors. diff --git a/apps/docs/content/guides/realtime/reports.mdx b/apps/docs/content/guides/realtime/reports.mdx index 0cf8e04208f..b17d012ecf2 100644 --- a/apps/docs/content/guides/realtime/reports.mdx +++ b/apps/docs/content/guides/realtime/reports.mdx @@ -57,15 +57,15 @@ height={625} ### Actions you can take -| Action | Description | More information | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Configure connection limit | Adjust the "Max concurrent connections" setting to increase or decrease the connection limit for your project | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/pricing) | -| Review quotas | Understand connection limits and other Realtime quotas for your plan | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | -| Understand connection quota | Learn how the concurrent connections quota works and how to configure it for your plan | [Concurrent Peak Connections Quota Troubleshooting](/docs/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp) | -| Fix silent disconnections | Fix connection issues in background applications using heartbeat callbacks and Web Workers | [Handling Silent Disconnections in Background Apps](/docs/troubleshooting/realtime-handling-silent-disconnections-in-backgrounded-applications-592794) | -| Check logs | Investigate connection errors and quota errors in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Contact support | Request custom quota increases for Enterprise plans or discuss connection requirements | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Configure connection limit | Adjust the "Max concurrent connections" setting to increase or decrease the connection limit for your project | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/pricing) | +| Review quotas | Understand connection limits and other Realtime quotas for your plan | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Understand connection quota | Learn how the concurrent connections quota works and how to configure it for your plan | [Concurrent Peak Connections Quota Troubleshooting](/docs/guides/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp) | +| Fix silent disconnections | Fix connection issues in background applications using heartbeat callbacks and Web Workers | [Handling Silent Disconnections in Background Apps](/docs/guides/troubleshooting/realtime-handling-silent-disconnections-in-backgrounded-applications-592794) | +| Check logs | Investigate connection errors and quota errors in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Contact support | Request custom quota increases for Enterprise plans or discuss connection requirements | [Support Portal](/dashboard/support/new) | ## Broadcast Events @@ -87,14 +87,14 @@ height={625} ### Actions you can take -| Action | Description | More information | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| Configure event limits | Adjust "Max events per second" and "Max payload size in KB" settings to optimize broadcast throughput and message size limits | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Review quotas | Understand message per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and broadcast payload size limits (Free: 256 KB, Pro+: 3,000 KB) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | -| Check logs | Investigate broadcast errors or quota limit issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Debug with logger | Enable logging to track messages sent and received, and diagnose broadcast delivery issues | [Debugging Realtime with Logger](/docs/troubleshooting/realtime-debugging-with-logger) | -| Learn broadcast basics | Understand how to implement and optimize broadcast messaging in your application | [Broadcast Guide](/docs/guides/realtime/broadcast) | -| Contact support | Request custom quota increases for Enterprise plans or discuss messaging requirements | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Configure event limits | Adjust "Max events per second" and "Max payload size in KB" settings to optimize broadcast throughput and message size limits | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Review quotas | Understand message per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and broadcast payload size limits (Free: 256 KB, Pro+: 3,000 KB) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Check logs | Investigate broadcast errors or quota limit issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Debug with logger | Enable logging to track messages sent and received, and diagnose broadcast delivery issues | [Debugging Realtime with Logger](/docs/guides/troubleshooting/realtime-debugging-with-logger) | +| Learn broadcast basics | Understand how to implement and optimize broadcast messaging in your application | [Broadcast Guide](/docs/guides/realtime/broadcast) | +| Contact support | Request custom quota increases for Enterprise plans or discuss messaging requirements | [Support Portal](/dashboard/support/new) | ## Presence Events @@ -116,14 +116,14 @@ height={625} ### Actions you can take -| Action | Description | More information | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| Configure presence limits | Adjust the "Max presence events per second" setting to optimize presence state update throughput | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Review quotas | Understand presence messages per second limits (Free: 20, Pro: 50, Pro no spend cap/Team/Enterprise: 1,000) and presence keys per object limits (10 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | -| Check logs | Investigate presence errors or quota limit issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Debug with logger | Enable logging to track presence events and diagnose state synchronization issues | [Debugging Realtime with Logger](/docs/troubleshooting/realtime-debugging-with-logger) | -| Learn presence basics | Understand how to implement and optimize presence state tracking in your application | [Presence Guide](/docs/guides/realtime/presence) | -| Contact support | Request custom quota increases for Enterprise plans or discuss presence requirements | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Configure presence limits | Adjust the "Max presence events per second" setting to optimize presence state update throughput | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Review quotas | Understand presence messages per second limits (Free: 20, Pro: 50, Pro no spend cap/Team/Enterprise: 1,000) and presence keys per object limits (10 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Check logs | Investigate presence errors or quota limit issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Debug with logger | Enable logging to track presence events and diagnose state synchronization issues | [Debugging Realtime with Logger](/docs/guides/troubleshooting/realtime-debugging-with-logger) | +| Learn presence basics | Understand how to implement and optimize presence state tracking in your application | [Presence Guide](/docs/guides/realtime/presence) | +| Contact support | Request custom quota increases for Enterprise plans or discuss presence requirements | [Support Portal](/dashboard/support/new) | ## Postgres Changes Events @@ -175,13 +175,13 @@ height={625} ### Actions you can take -| Action | Description | More information | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| Review quotas | Understand channel joins per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and channels per connection limits (100 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | -| Check logs | Investigate `too_many_joins` errors or channel join failures in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Fix channel errors | Learn how to properly manage channel lifecycle and prevent channel leaks in your application | [TooManyChannels Error Troubleshooting](/docs/troubleshooting/realtime-too-many-channels-error) | -| Learn channel basics | Understand how Realtime channels work and best practices for channel management | [Realtime Channels Concepts](/docs/guides/realtime/concepts#channels) | -| Contact support | Request custom quota increases for Enterprise plans or discuss high-volume channel join requirements | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| Review quotas | Understand channel joins per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and channels per connection limits (100 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Check logs | Investigate `too_many_joins` errors or channel join failures in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Fix channel errors | Learn how to properly manage channel lifecycle and prevent channel leaks in your application | [TooManyChannels Error Troubleshooting](/docs/guides/troubleshooting/realtime-too-many-channels-error) | +| Learn channel basics | Understand how Realtime channels work and best practices for channel management | [Realtime Channels Concepts](/docs/guides/realtime/concepts#channels) | +| Contact support | Request custom quota increases for Enterprise plans or discuss high-volume channel join requirements | [Support Portal](/dashboard/support/new) | ## Message Payload Size @@ -266,17 +266,17 @@ height={625} ### Actions you can take -| Action | Description | More information | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Configure connection pool | Adjust the "Database connection pool size" setting to increase the number of connections available for RLS authorization checks, which can improve performance for high-volume channel subscriptions | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Optimize RLS policies | Learn how to optimize RLS policies with indexes, function wrapping, and query optimization techniques | [RLS Performance Best Practices](/docs/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) | -| Check logs | Investigate RLS authorization errors or timeout issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Learn authorization basics | Understand how RLS policies work with private channels and best practices for implementation | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | -| Create indexes | Add indexes on columns frequently used in RLS policy conditions to speed up authorization checks | [Database Indexes Guide](/docs/guides/database/postgres/indexes) | -| Use index advisor | Automatically detect missing indexes that could improve RLS policy performance | [Index Advisor Extension Guide](/docs/guides/database/extensions/index_advisor) | -| Optimize queries | Learn techniques for optimizing queries including partial indexes and composite indexes for RLS conditions | [Query Optimization Guide](/docs/guides/database/query-optimization) | -| Monitor database | Review database query performance and identify slow queries that may be affecting RLS execution | [Database Observability Dashboard](/dashboard/project/_/observability/database) | -| Contact support | Discuss RLS optimization strategies or get assistance with complex authorization requirements | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Configure connection pool | Adjust the "Database connection pool size" setting to increase the number of connections available for RLS authorization checks, which can improve performance for high-volume channel subscriptions | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Optimize RLS policies | Learn how to optimize RLS policies with indexes, function wrapping, and query optimization techniques | [RLS Performance Best Practices](/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) | +| Check logs | Investigate RLS authorization errors or timeout issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Learn authorization basics | Understand how RLS policies work with private channels and best practices for implementation | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | +| Create indexes | Add indexes on columns frequently used in RLS policy conditions to speed up authorization checks | [Database Indexes Guide](/docs/guides/database/postgres/indexes) | +| Use index advisor | Automatically detect missing indexes that could improve RLS policy performance | [Index Advisor Extension Guide](/docs/guides/database/extensions/index_advisor) | +| Optimize queries | Learn techniques for optimizing queries including partial indexes and composite indexes for RLS conditions | [Query Optimization Guide](/docs/guides/database/query-optimization) | +| Monitor database | Review database query performance and identify slow queries that may be affecting RLS execution | [Database Observability Dashboard](/dashboard/project/_/observability/database) | +| Contact support | Discuss RLS optimization strategies or get assistance with complex authorization requirements | [Support Portal](/dashboard/support/new) | ## (Write) Private Channel Subscription RLS Execution Time @@ -299,17 +299,17 @@ height={625} ### Actions you can take -| Action | Description | More information | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Configure connection pool | Adjust the "Database connection pool size" setting to increase the number of connections available for RLS authorization checks, which can improve performance for high-frequency message publishing | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Optimize RLS policies | Learn how to optimize RLS policies with indexes, function wrapping, and query optimization techniques | [RLS Performance Best Practices](/docs/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) | -| Check logs | Investigate RLS authorization errors or timeout issues when publishing messages in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Learn authorization basics | Understand how RLS policies work with private channels for write operations and best practices for implementation | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | -| Create indexes | Add indexes on columns used in INSERT policies to speed up write authorization checks | [Database Indexes Guide](/docs/guides/database/postgres/indexes) | -| Use index advisor | Automatically detect missing indexes that could improve write RLS policy performance | [Index Advisor Extension Guide](/docs/guides/database/extensions/index_advisor) | -| Optimize queries | Learn techniques for optimizing INSERT policy queries including partial indexes for specific conditions | [Query Optimization Guide](/docs/guides/database/query-optimization) | -| Monitor database | Review database query performance and identify slow queries that may be affecting write RLS execution | [Database Observability Dashboard](/dashboard/project/_/observability/database) | -| Contact support | Discuss RLS optimization strategies or get assistance with complex authorization requirements for high-frequency messaging | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Configure connection pool | Adjust the "Database connection pool size" setting to increase the number of connections available for RLS authorization checks, which can improve performance for high-frequency message publishing | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Optimize RLS policies | Learn how to optimize RLS policies with indexes, function wrapping, and query optimization techniques | [RLS Performance Best Practices](/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) | +| Check logs | Investigate RLS authorization errors or timeout issues when publishing messages in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Learn authorization basics | Understand how RLS policies work with private channels for write operations and best practices for implementation | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | +| Create indexes | Add indexes on columns used in INSERT policies to speed up write authorization checks | [Database Indexes Guide](/docs/guides/database/postgres/indexes) | +| Use index advisor | Automatically detect missing indexes that could improve write RLS policy performance | [Index Advisor Extension Guide](/docs/guides/database/extensions/index_advisor) | +| Optimize queries | Learn techniques for optimizing INSERT policy queries including partial indexes for specific conditions | [Query Optimization Guide](/docs/guides/database/query-optimization) | +| Monitor database | Review database query performance and identify slow queries that may be affecting write RLS execution | [Database Observability Dashboard](/dashboard/project/_/observability/database) | +| Contact support | Discuss RLS optimization strategies or get assistance with complex authorization requirements for high-frequency messaging | [Support Portal](/dashboard/support/new) | ## Total Requests @@ -360,18 +360,18 @@ height={645} ### Actions you can take -| Action | Description | More information | -| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| Configure limits | Adjust "Max concurrent connections" or "Max events per second" settings if errors are related to quota limits being reached | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Check logs | Investigate specific error messages, error codes, and request details in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Review request volume | Compare error rates with total request volume to calculate error percentages and identify trends | [Total Requests Report](#total-requests) | -| Understand error codes | Understand specific error codes and their resolutions | [Realtime Error Codes Reference](/docs/guides/realtime/error_codes) | -| Learn HTTP status codes | Learn about HTTP status codes including 4XX client errors and 5XX server errors | [HTTP Status Codes Troubleshooting](/docs/troubleshooting/http-status-codes) | -| Fix timeout errors | Resolve WebSocket timeout errors caused by Node.js version incompatibility | [TIMED_OUT Connection Errors Troubleshooting](/docs/troubleshooting/realtime-connections-timed_out-status) | -| Understand heartbeats | Monitor heartbeat status to detect connection issues and handle timeouts | [Realtime Heartbeats Guide](/docs/troubleshooting/realtime-heartbeat-messages) | -| Review quotas | Check if errors are related to quota limits (e.g., `too_many_connections`, `too_many_joins`) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | -| Learn authorization | Troubleshoot authorization-related errors for private channels | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | -| Contact support | Get assistance with persistent errors or investigate service-level issues | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Configure limits | Adjust "Max concurrent connections" or "Max events per second" settings if errors are related to quota limits being reached | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Check logs | Investigate specific error messages, error codes, and request details in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Review request volume | Compare error rates with total request volume to calculate error percentages and identify trends | [Total Requests Report](#total-requests) | +| Understand error codes | Understand specific error codes and their resolutions | [Realtime Error Codes Reference](/docs/guides/realtime/error_codes) | +| Learn HTTP status codes | Learn about HTTP status codes including 4XX client errors and 5XX server errors | [HTTP Status Codes Troubleshooting](/docs/guides/troubleshooting/http-status-codes) | +| Fix timeout errors | Resolve WebSocket timeout errors caused by Node.js version incompatibility | [TIMED_OUT Connection Errors Troubleshooting](/docs/guides/troubleshooting/realtime-connections-timed_out-status) | +| Understand heartbeats | Monitor heartbeat status to detect connection issues and handle timeouts | [Realtime Heartbeats Guide](/docs/guides/troubleshooting/realtime-heartbeat-messages) | +| Review quotas | Check if errors are related to quota limits (e.g., `too_many_connections`, `too_many_joins`) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Learn authorization | Troubleshoot authorization-related errors for private channels | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | +| Contact support | Get assistance with persistent errors or investigate service-level issues | [Support Portal](/dashboard/support/new) | ## Response Speed @@ -393,13 +393,13 @@ height={834} ### Actions you can take -| Action | Description | More information | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| Configure connection pool | Adjust the "Database connection pool size" setting to optimize database connection usage, which can improve response times for authorization checks | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Check logs | Investigate slow requests and identify specific endpoints or operations causing delays | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | -| Review request volume | Correlate response times with request volume to identify performance degradation under load | [Total Requests Report](#total-requests) | -| Monitor database | Review database resource utilization, connection counts, and query performance that may affect response times | [Database Observability Dashboard](/dashboard/project/_/observability/database) | -| Review benchmarks | Understand expected latency and throughput for different Realtime operations | [Realtime Performance Benchmarks](/docs/guides/realtime/benchmarks) | -| Understand heartbeats | Monitor heartbeat status and customize intervals to balance detection speed with network overhead | [Realtime Heartbeats Guide](/docs/troubleshooting/realtime-heartbeat-messages) | -| Optimize RLS policies | If using private channels, optimize RLS policies that may be slowing down authorization checks | [RLS Performance Best Practices](/docs/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) | -| Contact support | Discuss performance optimization strategies or investigate persistent latency issues | [Support Portal](/dashboard/support/new) | +| Action | Description | More information | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| Configure connection pool | Adjust the "Database connection pool size" setting to optimize database connection usage, which can improve response times for authorization checks | [Realtime Settings Guide](/docs/guides/realtime/settings) | +| Check logs | Investigate slow requests and identify specific endpoints or operations causing delays | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | +| Review request volume | Correlate response times with request volume to identify performance degradation under load | [Total Requests Report](#total-requests) | +| Monitor database | Review database resource utilization, connection counts, and query performance that may affect response times | [Database Observability Dashboard](/dashboard/project/_/observability/database) | +| Review benchmarks | Understand expected latency and throughput for different Realtime operations | [Realtime Performance Benchmarks](/docs/guides/realtime/benchmarks) | +| Understand heartbeats | Monitor heartbeat status and customize intervals to balance detection speed with network overhead | [Realtime Heartbeats Guide](/docs/guides/troubleshooting/realtime-heartbeat-messages) | +| Optimize RLS policies | If using private channels, optimize RLS policies that may be slowing down authorization checks | [RLS Performance Best Practices](/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv) | +| Contact support | Discuss performance optimization strategies or investigate persistent latency issues | [Support Portal](/dashboard/support/new) | 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 9984e94d6c2..6afbb77b6ac 100644 --- a/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx +++ b/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx @@ -30,7 +30,7 @@ using ( true ); ### Create a trigger function -Let's create a function that we can call any time a record is created, updated, or deleted. This function will make use of some of Postgres's native [trigger variables](https://www.postgresql.org/docs/current/plpgsql-trigger.html#PLPGSQL-DML-TRIGGER). For this example, we want to have a topic with the name `topic:` to which we're going to broadcast events. +Create a function to call whenever a record is created, updated, or deleted. This function will make use of some of Postgres's native [trigger variables](https://www.postgresql.org/docs/current/plpgsql-trigger.html#PLPGSQL-DML-TRIGGER). For this example, we want to have a topic with the name `topic:` to which we're going to broadcast events. {/* prettier-ignore */} ```sql @@ -56,7 +56,7 @@ $$; ### Create a trigger -Let's set up a trigger so the function is executed after any changes to the table. +Set up a trigger so the function runs after any changes to the table. {/* prettier-ignore */} ```sql @@ -73,6 +73,7 @@ Finally, on the client side, listen to the topic `topic:` to receive ```js import { createClient } from '@supabase/supabase-js' + const supabase = createClient('your_project_url', 'your_supabase_api_key') // ---cut--- @@ -90,7 +91,7 @@ const changes = supabase ## Using Postgres Changes -Postgres Changes are simple to use, but have some [limitations](/docs/guides/realtime/postgres-changes#limitations) as your application scales. We recommend using Broadcast for most use cases. +Postgres Changes require minimal setup, but have some [limitations](/docs/guides/realtime/postgres-changes#limitations) as your application scales. We recommend using Broadcast for most use cases.