diff --git a/.claude/skills/clickhouse-logs-queries/SKILL.md b/.claude/skills/clickhouse-logs-queries/SKILL.md new file mode 100644 index 00000000000..91960c73179 --- /dev/null +++ b/.claude/skills/clickhouse-logs-queries/SKILL.md @@ -0,0 +1,226 @@ +--- +name: clickhouse-logs-queries +description: >- + Write, review, and migrate Supabase logs queries against the ClickHouse-backed + `logs` table (the `logs.all.otel` analytics endpoint). Use this whenever a task + involves Logs Explorer SQL, the `log_attributes` map, querying a log `source` + (edge_logs, postgres_logs, auth_logs, etc.), translating an old BigQuery + `cross join unnest(metadata)` logs query to ClickHouse, or wiring analytics log + SQL in `apps/studio/data/logs` and `apps/studio/components/interfaces/Settings/Logs`. + Reach for it even when the user just says "logs query", "Logs Explorer", or + pastes a BigQuery logs query to convert, not only when they name ClickHouse. +--- + +# Querying Supabase logs (ClickHouse) + +Supabase logs live in a single ClickHouse `logs` table, served by the +`logs.all.otel` analytics endpoint. Every log line from every part of the stack +is one row in this table, tagged by a `source` column. This replaces the older +BigQuery model, where each service had its own table and fields were reached +through `cross join unnest(metadata)`. + +Two kinds of work use this skill, and they share the same SQL model: + +1. **Writing or reviewing a logs query** (in the Logs Explorer or anywhere a raw + ClickHouse logs query is needed). Start here in this file. +2. **Wiring a logs query in the Studio codebase** (branded analytics SQL, the + endpoint picker, the OTEL query builders). Read + [references/codebase-integration.md](references/codebase-integration.md). + +If you are converting an existing BigQuery logs query, read +[references/bigquery-migration.md](references/bigquery-migration.md) for the full +translation table. + +## The logs table + +Each row has a small set of real columns. Everything specific to a service lives +in `log_attributes`. + +| Column | Type | Notes | +| ---------------- | --------------------- | ----------------------------------------------------- | +| `id` | `String` | Unique log identifier. | +| `timestamp` | `DateTime64` (UTC) | When the log was produced. Order/compare it directly. | +| `event_message` | `String` | The raw log line. | +| `severity_text` | `String` | Log level, when the source sets one. | +| `source` | `String` | The service the log came from. Always filter on this. | +| `log_attributes` | `Map(String, String)` | Structured per-source fields, keyed by a dotted path. | + +`timestamp` is formatted like `2026-06-22T09:34:06.215000` (ISO 8601, microsecond +precision, no trailing `Z`). In the Logs Explorer the selected time range is +applied for you, so you rarely need to write a `timestamp` filter by hand. + +A minimal, well-formed query. Lead with a comment naming the query, filter by +`source`, and always `limit`: + +```sql +-- recent edge requests +select timestamp, event_message +from logs +where source = 'edge_logs' +order by timestamp desc +limit 100; +``` + +## Sources + +`source` selects the service. The common ones: + +- `edge_logs` — API gateway requests and responses +- `postgres_logs` — database statements and errors (also where pg_cron logs live) +- `auth_logs` — authentication and authorization activity +- `function_edge_logs` — edge function requests and responses +- `function_logs` — `console` output from inside edge functions +- `storage_logs` — object upload and retrieval activity +- `realtime_logs` — Realtime client connections +- `postgrest_logs`, `supavisor_logs`, `pgbouncer_logs` — mostly `id`, `timestamp`, `event_message` + +The Logs Explorer **Field Reference** drawer lists every source and the fields it +actually sets. When in doubt about a key, discover it from real data rather than +guessing (see below). + +## Reading fields from log_attributes + +`log_attributes` maps a string key to a string value. Read a field with bracket +access. There are no unnesting joins: + +```sql +select + log_attributes['request.method'] as method, + log_attributes['request.path'] as path, + log_attributes['response.status_code'] as status +from logs +where source = 'edge_logs' +``` + +The key keeps the dotted path that BigQuery expressed through nested structs, with +the `metadata` root dropped: BigQuery `metadata.request.method` becomes +`log_attributes['request.method']`. Keep the full prefix — `request.cf.country` +is `log_attributes['request.cf.country']`, not `log_attributes['cf.country']`. + +Common keys by source: + +- `edge_logs`: `request.method`, `request.path`, `request.search`, `response.status_code`, `identifier` +- `postgres_logs`: `parsed.error_severity`, `parsed.detail`, `parsed.hint`, `parsed.query`, `identifier` +- `auth_logs`: `level`, `status`, `path`, `msg`, `error` +- `function_edge_logs`: `response.status_code`, `request.method`, `request.pathname`, `function_id`, `execution_id`, `execution_time_ms` +- `function_logs`: `event_type`, `function_id`, `execution_id`, `level` + +### Numeric fields are strings + +Map values are always strings. To compare or aggregate a numeric field, wrap it in +`toInt32OrZero`, which returns `0` for missing or non-numeric values so it never +errors on partial data: + +```sql +select count() as server_errors +from logs +where source = 'edge_logs' + and toInt32OrZero(log_attributes['response.status_code']) between 500 and 599 +``` + +### Discover the keys a source sets + +Read `mapKeys` from recent rows rather than guessing key names: + +```sql +select arrayJoin(mapKeys(log_attributes)) as key, count() as n +from logs +where source = 'postgres_logs' +group by key +order by n desc +limit 100; +``` + +`arrayJoin(mapKeys(...))` flattens the map keys into one row per key so you can +rank them by frequency. (The Studio codebase does exactly this for the Field +Reference drawer and to feed real keys to the AI rewrite.) + +## ClickHouse vs BigQuery functions + +These are the substitutions that trip people up most: + +| Need | BigQuery | ClickHouse | +| ------------------ | ----------------------------- | -------------------------------------------- | +| Count rows | `count(*)` | `count()` | +| Regex match | `regexp_contains(x, 'p')` | `match(x, 'p')` | +| Substring match | `x like '%p%'` | `x ilike '%p%'` (case-insensitive) or `like` | +| Numeric coercion | `cast(x as int64)` | `toInt32OrZero(x)` | +| Read the timestamp | `cast(timestamp as datetime)` | `timestamp` (use the column directly) | +| Map keys | n/a (used unnest) | `mapKeys(log_attributes)` | + +The `logs.all.otel` analytics endpoint (and the Logs Explorer on top of it) +rejects `count(*)` and `select *` — use `count()` and list the columns you need. +(Raw ClickHouse supports both; this is a constraint of the logs query surface.) + +## Best practices + +These keep queries correct and cheap. Log tables are large; an unbounded scan +reads far more data than you need. + +- **Start every query with an identifying comment** (e.g. `-- errors since last deploy`). It labels the query in logs and review, and makes each of several queries in a file easy to tell apart. +- **Always include a `LIMIT`.** Even for aggregates while you iterate. +- **Always query `from logs where source = '...'`.** There is no per-service table (no `edge_logs`, `postgres_logs`, etc. table) — there is one `logs` table, and `source` scopes it to a service. Filtering by `source` is required, not just an optimization. +- **Keep the time range tight.** A smaller window returns results faster. +- **Filter on the real columns** (`source`, `timestamp`) before reaching into + `log_attributes`. +- **Order by `timestamp desc`** to see the most recent logs first. +- **Use `count()`**, not `count(*)` or `select *`. + +## Worked examples + +Requests by status code: + +```sql +select + toInt32OrZero(log_attributes['response.status_code']) as status, + count() as count +from logs +where source = 'edge_logs' +group by status +order by count desc +limit 50 +``` + +Auth errors: + +```sql +select timestamp, event_message, log_attributes['msg'] as message +from logs +where source = 'auth_logs' + and log_attributes['level'] in ('error', 'fatal') +order by timestamp desc +limit 100 +``` + +Search the raw message: + +```sql +select timestamp, event_message +from logs +where source = 'postgres_logs' + and event_message ilike '%deadlock%' +order by timestamp desc +limit 100 +``` + +Postgres errors grouped by severity (the canonical unnest-to-map conversion): + +```sql +select log_attributes['parsed.error_severity'] as severity, count() as count +from logs +where source = 'postgres_logs' + and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC') +group by severity +order by count desc +limit 100 +``` + +## When the user pastes a BigQuery query + +Convert it rather than running it as-is. The mechanical steps (drop the +per-service table for `from logs where source = ...`, remove every +`cross join unnest(...)`, rewrite unnest-alias columns as `log_attributes['...']` +lookups, swap the functions above) are spelled out with a full before/after in +[references/bigquery-migration.md](references/bigquery-migration.md). The Logs +Explorer also has a built-in **Rewrite to ClickHouse** action that does this with +AI; point users to it for one-off conversions in the dashboard. diff --git a/.claude/skills/clickhouse-logs-queries/references/bigquery-migration.md b/.claude/skills/clickhouse-logs-queries/references/bigquery-migration.md new file mode 100644 index 00000000000..09d84f813b0 --- /dev/null +++ b/.claude/skills/clickhouse-logs-queries/references/bigquery-migration.md @@ -0,0 +1,93 @@ +# Migrating a BigQuery logs query to ClickHouse + +The old logs engine gave each service its own table and exposed structured fields +through repeated `cross join unnest(...)` over a nested `metadata` column. The +ClickHouse engine has one `logs` table and a flat `log_attributes` map. Conversion +is mechanical once you internalize the mapping. + +## The five steps + +1. **Replace the table with `logs` plus a `source` filter.** The old table name is + the `source` value: `from postgres_logs as t` becomes + `from logs where source = 'postgres_logs'`. Never select from a per-service + table name (`postgres_logs`, `edge_logs`, ...) on the ClickHouse engine. + - Exception: pg_cron logs live under `source = 'postgres_logs'`. + +2. **Remove every unnest join.** Delete `cross join unnest(metadata) as m`, + `cross join unnest(m.parsed) as p`, `left join unnest(...) on true`, and so on. + They have no equivalent — the data is already flat in the map. + +3. **Rewrite every unnest-alias column as a map lookup.** A field taken off + `unnest(metadata)` becomes `log_attributes['field']`. A field off a nested + struct like `unnest(m.parsed)` becomes `log_attributes['parsed.field']`: keep + the struct name as a dotted prefix, drop the `metadata` root and every alias. + +4. **Wrap numeric fields in `toInt32OrZero(...)`** before comparing or aggregating + them — map values are strings. + +5. **Swap BigQuery functions for ClickHouse ones** (see the table below). + +Preserve the original select list, filters, group by, order by, and limit intent +throughout. + +## Function substitutions + +| Need | BigQuery | ClickHouse | +| ---------------- | ----------------------------- | --------------------------- | +| Count rows | `count(*)` | `count()` | +| Regex match | `regexp_contains(x, 'p')` | `match(x, 'p')` | +| Substring match | `x like '%p%'` | `x ilike '%p%'` or `like` | +| Numeric coercion | `cast(x as int64)` | `toInt32OrZero(x)` | +| Timestamp value | `cast(timestamp as datetime)` | `timestamp` | +| All columns | `select *` | list the columns explicitly | + +## Full before/after + +BigQuery: + +```sql +select count(t.timestamp) as count, p.error_severity +from + postgres_logs as t + cross join unnest(metadata) as m + cross join unnest(m.parsed) as p +where p.error_severity in ('ERROR', 'FATAL', 'PANIC') +group by p.error_severity +order by count desc +limit 100; +``` + +ClickHouse: + +```sql +select count() as count, log_attributes['parsed.error_severity'] as error_severity +from logs +where source = 'postgres_logs' + and log_attributes['parsed.error_severity'] in ('ERROR', 'FATAL', 'PANIC') +group by log_attributes['parsed.error_severity'] +order by count desc +limit 100 +``` + +Notice: `count(t.timestamp)` became `count()`, the two unnest joins are gone, +`p.error_severity` became `log_attributes['parsed.error_severity']`, and the +`from`/`where` targets the single table. + +## Getting the keys right + +The most common conversion mistake is dropping a dotted prefix — writing +`log_attributes['x_real_ip']` when the real key is +`log_attributes['request.headers.x_real_ip']`, or `log_attributes['cf.country']` +instead of `log_attributes['request.cf.country']`. Do not invent or shorten keys. +When unsure, discover the real keys from data: + +```sql +select arrayJoin(mapKeys(log_attributes)) as key, count() as n +from logs +where source = 'edge_logs' +group by key +order by n desc +limit 200; +``` + +Then map each old nested path to the matching key exactly as it appears. diff --git a/.claude/skills/clickhouse-logs-queries/references/codebase-integration.md b/.claude/skills/clickhouse-logs-queries/references/codebase-integration.md new file mode 100644 index 00000000000..05c55fa9767 --- /dev/null +++ b/.claude/skills/clickhouse-logs-queries/references/codebase-integration.md @@ -0,0 +1,96 @@ +# Wiring a logs query in the Studio codebase + +This covers writing analytics log SQL inside `apps/studio` so it is safe, routed +to the right endpoint, and consistent with the existing OTEL builders. Read this +when you are editing TypeScript that builds or runs a logs query, not when you are +just writing a query in the Logs Explorer UI. + +## Branded SQL: never concatenate user input + +All analytics log SQL must be a `SafeLogSqlFragment`, built with the helpers in +`apps/studio/data/logs/safe-analytics-sql.ts`. This is enforced by eslint, and the +branding is what keeps interpolated values from becoming injection. The key +exports: + +- `safeSql\`...\``— a tagged template that only accepts`SafeLogSqlFragment`interpolations. Plain strings (and Postgres-branded`SafeSqlFragment`) are + rejected at compile time, so you cannot accidentally drop a raw value in. +- `analyticsLiteral(value)` — turns a string/number/boolean into a safely escaped + literal fragment (single quotes and backslashes are escaped). Use it for every + dynamic value, especially the `source`. +- `joinSqlFragments(fragments, separator)` — joins already-safe fragments with a + fixed structural separator (`' and '`, `', '`, etc.). +- `keyword(value, allowed)` — resolves a value against a compile-time allow-list of + fragments (e.g. an `AND`/`OR` operator). Returns the allow-listed fragment, never + the raw input. +- `quotedIdent(value)` — backtick-quotes a dotted identifier path after validating + each segment. + +There is intentionally no exported "raw" escape hatch. Compose with `safeSql` plus +these helpers. + +```ts +import { analyticsLiteral, safeSql } from 'data/logs/safe-analytics-sql' + +const source = 'edge_logs' +const sql = safeSql` + select timestamp, event_message + from logs + where source = ${analyticsLiteral(source)} + order by timestamp desc + limit 100 +` +``` + +## Pick the endpoint and builder by flag + +The ClickHouse path is gated by the `otelLegacyLogs` PostHog flag +(`useFlag('otelLegacyLogs')` from `common`). Keep the BigQuery path working when +the flag is off. Two helpers in `apps/studio/data/logs/logs-endpoint.ts` express +the split: + +- `logsAllEndpointUrl(useOtel)` — returns the `logs.all.otel` endpoint when + `useOtel`, otherwise the legacy `logs.all` endpoint. +- `pickLogsQueryBuilder(useOtel, otel, bq)` — picks between an OTEL builder and a + BigQuery builder while preserving the type. + +```ts +const useOtel = useFlag('otelLegacyLogs') +const builder = pickLogsQueryBuilder(useOtel, genDefaultQueryOtel, genDefaultQuery) +const endpoint = logsAllEndpointUrl(useOtel) +// include { otel: useOtel } in the React Query key so the two paths cache separately +``` + +Run the fragment through `executeAnalyticsSql` (`apps/studio/data/logs/execute-analytics-sql.ts`) +against that endpoint. + +## Follow the existing OTEL builders + +When you need a new query shape, mirror the generators in +`apps/studio/components/interfaces/Settings/Logs/Logs.utils.otel.ts` rather than +inventing a parallel style. They already encode the conventions: + +- `genDefaultQueryOtel`, `genCountQueryOtel`, `genChartQueryOtel`, + `genSingleLogQueryOtel` — the row/count/chart/single-log builders. They select + the real columns plus source-specific `log_attributes[...]` lookups aliased to + the leaf names the renderers expect. +- `mapOtelPreviewRow`, `mapOtelSingleLogToLegacy`, `otelTimestampToMicros` — the + JS normalization layer. `timestamp` must end up a microsecond number for the + pagination cursor and the renderers, so reuse `otelTimestampToMicros` rather + than parsing the ISO string yourself. + +The map-key discovery hook (`apps/studio/data/logs/otel-log-keys-query.ts`, +`fetchOtelLogKeys` / `useOtelLogKeysQuery`) is the canonical example of a small, +correctly-branded OTEL query — read it before writing a new one. + +## Checklist + +- [ ] Every dynamic value goes through `analyticsLiteral` (or another sanitizer), + never string concatenation. +- [ ] The query filters by `source` and includes a `LIMIT`. +- [ ] Numeric `log_attributes` values are wrapped in `toInt32OrZero`. +- [ ] The endpoint and builder are chosen with `logsAllEndpointUrl` / + `pickLogsQueryBuilder` off `useFlag('otelLegacyLogs')`. +- [ ] The React Query key distinguishes the OTEL and BigQuery paths. +- [ ] `timestamp` is normalized to micros for any row consumed by the table/cursor. +- [ ] There is a unit test asserting the generated SQL string (see + `Logs.utils.otel.test.ts` and `safe-analytics-sql.test.ts` for the pattern). diff --git a/.claude/skills/studio-e2e-tests/SKILL.md b/.claude/skills/studio-e2e-tests/SKILL.md index 1006981e42d..ecd48bc8376 100644 --- a/.claude/skills/studio-e2e-tests/SKILL.md +++ b/.claude/skills/studio-e2e-tests/SKILL.md @@ -88,7 +88,7 @@ test.describe.configure({ mode: 'serial' }) 3. **`getByText` with exact match** - Good for unique text ```typescript - page.getByText('Data API Access', { exact: true }) + page.getByText('Data API access', { exact: true }) ``` 4. **`locator` with CSS** - Use sparingly, more fragile diff --git a/.claude/skills/studio-mock-api-tests/SKILL.md b/.claude/skills/studio-mock-api-tests/SKILL.md index bd09ea125f6..f073e305750 100644 --- a/.claude/skills/studio-mock-api-tests/SKILL.md +++ b/.claude/skills/studio-mock-api-tests/SKILL.md @@ -99,7 +99,7 @@ response: () => HttpResponse.json({}, { status: 201 }) ### 3. Submit buttons in Sheets/Modals need `fireEvent.click` -The convention ` diff --git a/apps/design-system/components/code-fragment.tsx b/apps/design-system/components/code-fragment.tsx index ccf33703720..4eeb7ee7c7c 100644 --- a/apps/design-system/components/code-fragment.tsx +++ b/apps/design-system/components/code-fragment.tsx @@ -94,10 +94,10 @@ export function CodeFragment({ )} > {showGrid && ( -
+
)} {showDottedGrid && ( -
+
)}
{ComponentPreview}
diff --git a/apps/design-system/components/command-menu.tsx b/apps/design-system/components/command-menu.tsx index 2fa3cfee17f..e4731f2a055 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. @@ -107,7 +105,7 @@ Used for actions that are not as important as the primary action, or for actions -### Only an Icon +### Only an icon Displaying only an Icon in a button. @@ -117,12 +115,28 @@ Displaying only an Icon in a button. -### As Child +### As child Supports slot behavior with `asChild` prop. +### Split with dropdown + +Pair a button with a chevron `DropdownMenu` trigger when there are variations of the same action, or alternative ways to accomplish the same goal. The default or most likely option should be used on the exposed button. + +When secondary actions are related but distinct—not alternatives to the primary action—display the primary action as a button and place the rest in an overflow menu instead. See [Table multiple actions](./table#multiple-actions). + + + +The shared middle border is the tricky part. Do **not** use `border-l-0` on the chevron button — that drops the divider on hover/focus. Instead: + +- Primary: `rounded-r-none` and `hover:z-10` so its border stacks above the chevron on hover. +- Chevron trigger: `rounded-l-none`, `shrink-0`, `px-[4px] py-[5px]`, and `-ml-px` to overlap the adjacent border by one pixel. +- Chevron trigger only: `aria-label` describing the menu (the icon is decorative). + +Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions, also use `flex w-full @lg:w-auto` with `flex-1 @lg:flex-none` on the primary when `layout="responsive"`. + ## Accessibility [Keyboard focus](../accessibility#focus-management) is automatically handled: diff --git a/apps/design-system/content/docs/components/calendar.mdx b/apps/design-system/content/docs/components/calendar.mdx index 9b1d4187cbb..dff3e95d183 100644 --- a/apps/design-system/content/docs/components/calendar.mdx +++ b/apps/design-system/content/docs/components/calendar.mdx @@ -79,3 +79,9 @@ You can use the `` component to build a date picker. See the [Date Pic ### Form + +### Calendar with disabled days + +Shows a calendar with disabled days for selection. The current month starts mid week. + + 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/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/package.json b/apps/design-system/package.json index 5f8269c8af0..9636c6b1dd6 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -5,20 +5,20 @@ "type": "module", "scripts": { "preinstall": "npx only-allow pnpm", - "dev": "next dev --turbopack --port 3003", - "dev:full": "concurrently \"pnpm dev\" \"pnpm content:dev\"", + "dev": "run-p --race dev:*", + "dev:next": "next dev --turbopack --port 3003", + "dev:content": "contentlayer2 dev", "build": "pnpm run content:build && pnpm run build:registry && next build --turbopack", "build:registry": "tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache", "start": "next start", "lint": "eslint .", - "content:dev": "contentlayer2 dev", "content:build": "contentlayer2 build", "clean": "rimraf node_modules .next .turbo", "typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json" }, "dependencies": { "@hookform/resolvers": "^3.1.1", - "@tanstack/react-table": "^8.21.3", + "@tanstack/react-table": "catalog:", "contentlayer2": "0.4.6", "common": "workspace:*", "date-fns": "^2.30.0", @@ -57,15 +57,16 @@ "@types/lodash.template": "4.5.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "concurrently": "^8.2.2", "config": "workspace:*", "mdast-util-toc": "^6.1.1", + "npm-run-all": "^4.1.5", "postcss": "catalog:", "rimraf": "^4.1.3", "shiki": "^1.1.7", "tailwindcss": "catalog:", "tsconfig": "workspace:*", "tsx": "catalog:", + "@typescript/native": "catalog:", "typescript": "catalog:", "unist-builder": "3.0.0" } 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-split.tsx b/apps/design-system/registry/default/example/admonition-button-split.tsx new file mode 100644 index 00000000000..3432c96f5c5 --- /dev/null +++ b/apps/design-system/registry/default/example/admonition-button-split.tsx @@ -0,0 +1,60 @@ +import { ChevronDown } from 'lucide-react' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +export default function AdmonitionButtonSplitDemo() { + return ( + + + + + } + 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/breadcrumb-demo.tsx b/apps/design-system/registry/default/example/breadcrumb-demo.tsx new file mode 100644 index 00000000000..776ad47761d --- /dev/null +++ b/apps/design-system/registry/default/example/breadcrumb-demo.tsx @@ -0,0 +1,28 @@ +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from 'ui' + +export default function BreadcrumbDemo() { + return ( + + + + Home + + + + Components + + + + Breadcrumb + + + + ) +} diff --git a/apps/design-system/registry/default/example/breadcrumb-dropdown.tsx b/apps/design-system/registry/default/example/breadcrumb-dropdown.tsx new file mode 100644 index 00000000000..b1d12a14874 --- /dev/null +++ b/apps/design-system/registry/default/example/breadcrumb-dropdown.tsx @@ -0,0 +1,50 @@ +import { ChevronDown, Slash } from 'lucide-react' +import Link from 'next/link' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' + +export default function BreadcrumbDropdownDemo() { + return ( + + + + + Home + + + + + + + + + Components + + + + Documentation + Themes + GitHub + + + + + + + + Breadcrumb + + + + ) +} diff --git a/apps/design-system/registry/default/example/breadcrumb-ellipsis.tsx b/apps/design-system/registry/default/example/breadcrumb-ellipsis.tsx new file mode 100644 index 00000000000..e291bc8b528 --- /dev/null +++ b/apps/design-system/registry/default/example/breadcrumb-ellipsis.tsx @@ -0,0 +1,38 @@ +import Link from 'next/link' +import { + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from 'ui' + +export default function BreadcrumbEllipsisDemo() { + return ( + + + + + Home + + + + + + + + + + Components + + + + + Breadcrumb + + + + ) +} diff --git a/apps/design-system/registry/default/example/breadcrumb-link.tsx b/apps/design-system/registry/default/example/breadcrumb-link.tsx new file mode 100644 index 00000000000..900fa9c8606 --- /dev/null +++ b/apps/design-system/registry/default/example/breadcrumb-link.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from 'ui' + +export default function BreadcrumbLinkDemo() { + return ( + + + + + Home + + + + + + Components + + + + + Breadcrumb + + + + ) +} diff --git a/apps/design-system/registry/default/example/breadcrumb-responsive.tsx b/apps/design-system/registry/default/example/breadcrumb-responsive.tsx new file mode 100644 index 00000000000..5e43731580f --- /dev/null +++ b/apps/design-system/registry/default/example/breadcrumb-responsive.tsx @@ -0,0 +1,137 @@ +'use client' + +import Link from 'next/link' +import * as React from 'react' +import { + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, + Button, + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' + +function useMediaQuery(query: string) { + const [value, setValue] = React.useState(false) + + React.useEffect(() => { + function onChange(event: MediaQueryListEvent) { + setValue(event.matches) + } + + const result = matchMedia(query) + result.addEventListener('change', onChange) + setValue(result.matches) + + return () => result.removeEventListener('change', onChange) + }, [query]) + + return value +} + +const items = [ + { href: '#', label: 'Home' }, + { href: '#', label: 'Documentation' }, + { href: '#', label: 'Build Your Application' }, + { href: '#', label: 'Data Fetching' }, + { label: 'Caching and Revalidating' }, +] + +const ITEMS_TO_DISPLAY = 3 + +export default function BreadcrumbResponsiveDemo() { + const [open, setOpen] = React.useState(false) + const isDesktop = useMediaQuery('(min-width: 768px)') + + return ( + + + + + {items[0].label} + + + + {items.length > ITEMS_TO_DISPLAY ? ( + <> + + {isDesktop ? ( + + + + + + {items.slice(1, -2).map((item, index) => ( + + {item.label} + + ))} + + + ) : ( + + + + + + + Navigate to + Select a page to navigate to. + +
+ {items.slice(1, -2).map((item, index) => ( + + {item.label} + + ))} +
+ + + + + +
+
+ )} +
+ + + ) : null} + {items.slice(-ITEMS_TO_DISPLAY + 1).map((item, index) => ( + + {index > 0 ? : null} + + {item.href ? ( + + {item.label} + + ) : ( + + {item.label} + + )} + + + ))} +
+
+ ) +} diff --git a/apps/design-system/registry/default/example/breadcrumb-separator.tsx b/apps/design-system/registry/default/example/breadcrumb-separator.tsx new file mode 100644 index 00000000000..9de9e3583eb --- /dev/null +++ b/apps/design-system/registry/default/example/breadcrumb-separator.tsx @@ -0,0 +1,38 @@ +import { Slash } from 'lucide-react' +import Link from 'next/link' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from 'ui' + +export default function BreadcrumbSeparatorDemo() { + return ( + + + + + Home + + + + + + + + Components + + + + + + + Breadcrumb + + + + ) +} 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-split-dropdown.tsx b/apps/design-system/registry/default/example/button-split-dropdown.tsx new file mode 100644 index 00000000000..885e1bbebaa --- /dev/null +++ b/apps/design-system/registry/default/example/button-split-dropdown.tsx @@ -0,0 +1,37 @@ +import { ChevronDown } from 'lucide-react' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from 'ui' + +export default function ButtonSplitDropdownDemo() { + 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-disabled-days-demo.tsx b/apps/design-system/registry/default/example/calendar-disabled-days-demo.tsx new file mode 100644 index 00000000000..f428f871cb2 --- /dev/null +++ b/apps/design-system/registry/default/example/calendar-disabled-days-demo.tsx @@ -0,0 +1,25 @@ +'use client' + +import * as React from 'react' +import { Calendar } from 'ui' + +// The dates are specifically chosen to show a month which starts mid week. +const latestDate = new Date(2026, 6, 10) +const earliestDate = new Date(2026, 6, 7) + +export default function CalendarMidWeekDemo() { + const [date, setDate] = React.useState(earliestDate) + + 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..50f25046cb2 100644 --- a/apps/design-system/registry/default/example/copy-empty-states.tsx +++ b/apps/design-system/registry/default/example/copy-empty-states.tsx @@ -2,7 +2,7 @@ import { Key } from 'lucide-react' import { Button } from 'ui' -import { EmptyStatePresentational } from 'ui-patterns' +import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' export default function CopyEmptyStates() { return ( @@ -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..1208680a3d0 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..09a85c5d057 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 @@ -1,6 +1,6 @@ import { Plus } from 'lucide-react' import { Button } from 'ui' -import { EmptyStatePresentational } from 'ui-patterns' +import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' export default function EmptyStatePresentationalIcon() { return ( @@ -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..a25204ae1ca 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 @@ -1,7 +1,7 @@ import { BucketPlus } from 'icons' import { Plus } from 'lucide-react' import { Button } from 'ui' -import { EmptyStatePresentational } from 'ui-patterns' +import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' export default function EmptyStatePresentationalIcon() { return ( @@ -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..4bc48b1bfec 100644 --- a/apps/design-system/registry/default/example/filter-bar-demo.tsx +++ b/apps/design-system/registry/default/example/filter-bar-demo.tsx @@ -2,7 +2,7 @@ import { format } from 'date-fns' import { useState } from 'react' import { DateRange } from 'react-day-picker' import { Button, Calendar } from 'ui' -import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns' +import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns/FilterBar' function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) { const [date, setDate] = useState( @@ -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 20cd3d30009..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' @@ -73,7 +76,7 @@ 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 .union([ @@ -324,19 +327,20 @@ export default function FormPatternsPageLayout() { >
- +
{logoUrl && ( - - + + + - - + + )} @@ -757,13 +757,13 @@ export default function FormPatternsPageLayout() { >
-
@@ -771,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 6c729295442..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' @@ -70,7 +72,7 @@ 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 .union([ @@ -143,7 +145,7 @@ export default function FormPatternsSidePanel() { return ( <> - @@ -316,7 +318,7 @@ export default function FormPatternsSidePanel() {
{logoUrl && ( - - + + + - - + + )} @@ -769,13 +768,13 @@ export default function FormPatternsSidePanel() { >
-
@@ -785,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..4377bee9ec1 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 @@ -150,7 +150,7 @@ const buildMockChartData = (intervalKey: string) => { const EXECUTION_TIME_CHART_CONFIG = { avg_execution_time: { label: 'Average Execution Time', - color: 'hsl(var(--foreground-default))', + color: 'var(--foreground-default)', }, max_execution_time: { label: 'Max Execution Time', @@ -234,10 +234,10 @@ export default function PageLayoutEdgeFunction() { - - @@ -394,7 +394,7 @@ function OverviewPage() { {CHART_INTERVALS.map((item, index) => ( @@ -485,7 +485,7 @@ function OverviewPage() { { y: averageExecutionTime, label: 'average', - stroke: 'hsl(var(--foreground-default))', + stroke: 'var(--foreground-default)', strokeWidth: 1.5, }, ]} @@ -538,7 +538,7 @@ function OverviewPage() { { y: averageCpuTime, label: 'average', - stroke: 'hsl(var(--foreground-default))', + stroke: 'var(--foreground-default)', strokeWidth: 1.5, }, ]} @@ -589,7 +589,7 @@ function OverviewPage() { { y: averageMemoryUsage, label: 'average', - stroke: 'hsl(var(--foreground-default))', + stroke: 'var(--foreground-default)', strokeWidth: 1.5, }, ]} @@ -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..8879095879c 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 @@ -2,7 +2,7 @@ import { useState } from 'react' import { Button, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' -import { FilterBar, isGroup, type FilterCondition, type FilterGroup } from 'ui-patterns' +import { FilterBar, isGroup, type FilterCondition, type FilterGroup } from 'ui-patterns/FilterBar' const logs = [ ['10:42:01.129', 'POST /hello-world', '200', '132ms', 'iad1'], @@ -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} -
- - + + Password @@ -66,7 +66,7 @@ export default function TabsDemo() { - - + + ) } diff --git a/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx b/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx index 09292b5bfa0..5218106f9f2 100644 --- a/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx +++ b/apps/design-system/registry/default/example/text-confirm-dialog-demo.tsx @@ -10,7 +10,7 @@ export default function TextConfirmDialogDemo() { return ( <> - 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/toc-demo.tsx b/apps/design-system/registry/default/example/toc-demo.tsx index 6764c55119f..f62d944a7aa 100644 --- a/apps/design-system/registry/default/example/toc-demo.tsx +++ b/apps/design-system/registry/default/example/toc-demo.tsx @@ -1,7 +1,13 @@ 'use client' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' -import { AnchorProvider, Toc, TOCItems, TOCScrollArea, type AnchorProviderProps } from 'ui-patterns' +import { + AnchorProvider, + Toc, + TOCItems, + TOCScrollArea, + type AnchorProviderProps, +} from 'ui-patterns/Toc' export default function MultiSelectDemo() { return ( diff --git a/apps/design-system/registry/default/example/toc-single-demo.tsx b/apps/design-system/registry/default/example/toc-single-demo.tsx index bb010262eae..513b3e88c20 100644 --- a/apps/design-system/registry/default/example/toc-single-demo.tsx +++ b/apps/design-system/registry/default/example/toc-single-demo.tsx @@ -1,7 +1,13 @@ 'use client' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' -import { AnchorProvider, Toc, TOCItems, TOCScrollArea, type AnchorProviderProps } from 'ui-patterns' +import { + AnchorProvider, + Toc, + TOCItems, + TOCScrollArea, + type AnchorProviderProps, +} from 'ui-patterns/Toc' export default function MultiSelectDemo() { 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 596c4fbb5a0..7e05ca4755f 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -25,6 +25,12 @@ export const examples: Registry = [ registryDependencies: ['admonition'], files: ['example/admonition-button.tsx'], }, + { + name: 'admonition-button-split', + type: 'components:example', + registryDependencies: ['admonition', 'button', 'dropdown-menu'], + files: ['example/admonition-button-split.tsx'], + }, { name: 'admonition-description-only', type: 'components:example', @@ -139,42 +145,42 @@ export const examples: Registry = [ registryDependencies: ['badge'], files: ['example/badge-secondary.tsx'], }, - // { - // name: 'breadcrumb-demo', - // type: 'components:example', - // registryDependencies: ['breadcrumb'], - // files: ['example/breadcrumb-demo.tsx'], - // }, - // { - // name: 'breadcrumb-separator', - // type: 'components:example', - // registryDependencies: ['breadcrumb'], - // files: ['example/breadcrumb-separator.tsx'], - // }, - // { - // name: 'breadcrumb-dropdown', - // type: 'components:example', - // registryDependencies: ['breadcrumb'], - // files: ['example/breadcrumb-dropdown.tsx'], - // }, - // { - // name: 'breadcrumb-ellipsis', - // type: 'components:example', - // registryDependencies: ['breadcrumb'], - // files: ['example/breadcrumb-ellipsis.tsx'], - // }, - // { - // name: 'breadcrumb-link', - // type: 'components:example', - // registryDependencies: ['breadcrumb'], - // files: ['example/breadcrumb-link.tsx'], - // }, - // { - // name: 'breadcrumb-responsive', - // type: 'components:example', - // registryDependencies: ['breadcrumb'], - // files: ['example/breadcrumb-responsive.tsx'], - // }, + { + name: 'breadcrumb-demo', + type: 'components:example', + registryDependencies: ['breadcrumb'], + files: ['example/breadcrumb-demo.tsx'], + }, + { + name: 'breadcrumb-separator', + type: 'components:example', + registryDependencies: ['breadcrumb'], + files: ['example/breadcrumb-separator.tsx'], + }, + { + name: 'breadcrumb-dropdown', + type: 'components:example', + registryDependencies: ['breadcrumb', 'dropdown-menu'], + files: ['example/breadcrumb-dropdown.tsx'], + }, + { + name: 'breadcrumb-ellipsis', + type: 'components:example', + registryDependencies: ['breadcrumb'], + files: ['example/breadcrumb-ellipsis.tsx'], + }, + { + name: 'breadcrumb-link', + type: 'components:example', + registryDependencies: ['breadcrumb'], + files: ['example/breadcrumb-link.tsx'], + }, + { + name: 'breadcrumb-responsive', + type: 'components:example', + registryDependencies: ['breadcrumb', 'button', 'drawer', 'dropdown-menu'], + files: ['example/breadcrumb-responsive.tsx'], + }, { name: 'button-demo', type: 'components:example', @@ -253,6 +259,12 @@ export const examples: Registry = [ registryDependencies: ['button'], files: ['example/button-as-child.tsx'], }, + { + name: 'button-split-dropdown', + type: 'components:example', + registryDependencies: ['button', 'dropdown-menu'], + files: ['example/button-split-dropdown.tsx'], + }, { name: 'calendar-demo', type: 'components:example', @@ -265,6 +277,12 @@ export const examples: Registry = [ registryDependencies: ['calendar', 'form', 'popover'], files: ['example/calendar-form.tsx'], }, + { + name: 'calendar-disabled-days-demo', + type: 'components:example', + registryDependencies: ['calendar', 'form', 'popover'], + files: ['example/calendar-disabled-days-demo.tsx'], + }, { name: 'single-value-field-array-demo', type: 'components:example', diff --git a/apps/design-system/styles/code-block-variables.css b/apps/design-system/styles/code-block-variables.css index f1054071748..a21af6b3751 100644 --- a/apps/design-system/styles/code-block-variables.css +++ b/apps/design-system/styles/code-block-variables.css @@ -18,16 +18,16 @@ [data-theme='light'], .light { --code-token-keyword: #6b35dc; - --code-foreground: hsl(var(--foreground-light) / 1); + --code-foreground: oklch(from var(--foreground-light) l c h / 1); --code-token-constant: #15593b; - --code-token-string: #f1a10d; + --code-token-string: #c46a0a; --code-token-comment: #7e7e7e; - --code-token-parameter: hsl(var(--foreground-light) / 1); + --code-token-parameter: oklch(from var(--foreground-light) l c h / 1); --code-token-function: #15593b; - --code-token-string-expression: #f1a10d; - --code-token-punctuation: hsl(var(--foreground-light) / 1); - --code-token-link: hsl(var(--foreground-light) / 1); - --code-token-number: hsl(var(--foreground-light) / 1); + --code-token-string-expression: #c46a0a; + --code-token-punctuation: oklch(from var(--foreground-light) l c h / 1); + --code-token-link: oklch(from var(--foreground-light) l c h / 1); + --code-token-number: oklch(from var(--foreground-light) l c h / 1); --code-token-property: #15593b; --code-highlight-color: #1c1c1c; } diff --git a/apps/docs/.env.development b/apps/docs/.env.development index f5e09eed460..56120419134 100644 --- a/apps/docs/.env.development +++ b/apps/docs/.env.development @@ -9,6 +9,10 @@ NEXT_PUBLIC_BASE_PATH="/docs" # Setting this to true requires certain secret keys NEXT_PUBLIC_IS_PLATFORM="false" +# Base URL for the hosted MCP server. Overrides the DEFAULT_MCP_URL_PLATFORM +# fallback in @ui-patterns/McpUrlBuilder. +NEXT_PUBLIC_MCP_URL="http://localhost:8080/mcp" + # Supabase project containing integration information NEXT_PUBLIC_MISC_URL="https://obuldanrptloktxcffvn.supabase.co" NEXT_PUBLIC_MISC_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTg2MTQ2ODUsImV4cCI6MjAzNDE5MDY4NX0.NFt49g6DFkc1X5khCzN5p01iAVo2TMxlx88cY1V0E2M" diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index eb1e923d280..5dbb96adacc 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -32,6 +32,7 @@ public/llms/ # Generated guide and reference markdown files public/markdown/ public/docs.tar.gz +public/docs/ # Copied examples folder /examples/ diff --git a/apps/docs/CONTRIBUTING.md b/apps/docs/CONTRIBUTING.md index 6450a90c812..6a3027b1770 100644 --- a/apps/docs/CONTRIBUTING.md +++ b/apps/docs/CONTRIBUTING.md @@ -197,15 +197,42 @@ Keep code lines short to avoid scrolling. For example, you can split long shell Optionally specify a filename for the codeblock by including it after the opening backticks and language specifier: -```md +````md ```ts environment.ts + ``` +```` Optionally highlight lines by using `mark=${lineNumber}`. -```md +````md ```js mark=12:13 + ``` +```` + +### Content listings + +Overview and index pages use a single `` component for curated link sections such as "Get started", "Next steps", "Examples", or "Resources". Refer to [`storage.data.ts`](data/content-listings/storage.data.ts) and [`storage.mdx`](content/guides/storage.mdx) for a full example. + +**Prompt to add content listings:** + +```text +Add a content listing block for [TOPIC] / [SECTION] (for example, Storage / Examples). +Follow CONTRIBUTING § Content listings in apps/docs. +Copy structure from `storageGetStarted` in apps/docs/data/content-listings/storage.data.ts. +Pick a globally-unique kebab-case id like `[topic]-[section]`. +Run `pnpm test:local lib/content-listings.test.ts` from apps/docs. +``` + +**Manually add content listings:** + +1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups (e.g. `storage-get-started`, not just `get-started`) — it is used both as the lookup key and as the telemetry `listingId`. +2. Place the component inline in guide MDX, for example ``. Use a partial only when the block is reused or gated with `$Show` at the partial level. +3. Run `pnpm test:local lib/content-listings.test.ts` from `apps/docs`. + +Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets): `cl-data` (data export with namespaced id) and `cl-inline` (MDX component). + ### Footnotes @@ -284,7 +311,7 @@ Don't nest lists more than two deep. 3. List item - List item - List item - + - Overly nested list item ``` diff --git a/apps/docs/app/api/guides-md/[...slug]/route.ts b/apps/docs/app/api/guides-md/[...slug]/route.ts index 51dea28525c..f29c8258538 100644 --- a/apps/docs/app/api/guides-md/[...slug]/route.ts +++ b/apps/docs/app/api/guides-md/[...slug]/route.ts @@ -18,6 +18,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug headers: { 'Content-Type': 'text/markdown; charset=utf-8', 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=3600', + Vary: 'Accept', }, }) } catch { diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index a4c0476d5d4..12d02b26a59 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -20,13 +20,11 @@ For content that requires progressive disclosure: ```mdx -
-
-
-
``` -
- @@ -69,9 +62,7 @@ For content that requires progressive disclosure: -
-
- @@ -80,7 +71,6 @@ For content that requires progressive disclosure: -
### Admonition @@ -277,6 +267,7 @@ You can also import the `supabase-js` library here: ````mdx ```js import { createClient } from '@supabase/supabase-js' + const supabase = createClient('dummy', 'client') // ---cut--- @@ -291,6 +282,7 @@ Note the hidden statements above the cut. Hover over `signInWithPassword` to see ```js import { createClient } from '@supabase/supabase-js' + const supabase = createClient('dummy', 'client') // ---cut--- @@ -507,14 +499,12 @@ We incorporate content reuse in the docs to avoid duplication. If you find yours -
- @@ -527,9 +517,7 @@ We incorporate content reuse in the docs to avoid duplication. If you find yours -
-
- @@ -542,7 +530,6 @@ We incorporate content reuse in the docs to avoid duplication. If you find yours -
To make a new partial: 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/ai-tools/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx index df39f6ee8eb..7eaf8f3ceae 100644 --- a/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx +++ b/apps/docs/app/guides/ai-tools/ai-prompts/[slug]/page.tsx @@ -6,8 +6,8 @@ import { wrapInMarkdownCodeBlock, } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { source } from 'common-tags' +import { notFound } from 'next/navigation' export const dynamicParams = false @@ -18,7 +18,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str const prompt = await getAiPrompt(slug) if (!prompt) { - notFoundWithPathname(`/guides/ai-tools/ai-prompts/${slug}`) + notFound() } let { heading, content } = prompt diff --git a/apps/docs/app/guides/ai/python/[slug]/page.tsx b/apps/docs/app/guides/ai/python/[slug]/page.tsx index e64133a1c39..e31bc6b1595 100644 --- a/apps/docs/app/guides/ai/python/[slug]/page.tsx +++ b/apps/docs/app/guides/ai/python/[slug]/page.tsx @@ -1,13 +1,14 @@ +import { notFound } from 'next/navigation' import { relative } from 'path' +import rehypeSlug from 'rehype-slug' + import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' -import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { getGitHubFileContents } from '~/lib/octokit' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' -import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import rehypeSlug from 'rehype-slug' export const dynamicParams = false @@ -75,7 +76,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) if (!page) { - notFoundWithPathname(`/guides/ai/python/${slug}`) + notFound() } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/database/database-advisors/page.tsx b/apps/docs/app/guides/database/database-advisors/page.tsx index 56d9c89876b..946afb6d46c 100644 --- a/apps/docs/app/guides/database/database-advisors/page.tsx +++ b/apps/docs/app/guides/database/database-advisors/page.tsx @@ -11,7 +11,7 @@ import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import { capitalize } from 'lodash-es' import rehypeSlug from 'rehype-slug' import { Heading } from 'ui' -import { Admonition } from 'ui-patterns' +import { Admonition } from 'ui-patterns/admonition' // We fetch these docs at build time from an external repo const org = 'supabase' 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 f85355e895a..c8236dcddff 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -5,7 +5,6 @@ import { genGuidesStaticParams, removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { newEditLink } from '~/features/helpers.edit-link' import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide' // End of third-party imports @@ -21,10 +20,11 @@ import type { SerializeOptions } from '~/types/next-mdx-remote-serialize' import { isFeatureEnabled } from 'common' import matter from 'gray-matter' import Link from 'next/link' +import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' import emoji from 'remark-emoji' import { Button } from 'ui' -import { Admonition } from 'ui-patterns' +import { Admonition } from 'ui-patterns/admonition' // We fetch these docs at build time from an external repo const org = 'supabase' @@ -223,6 +223,14 @@ const pageMap = [ }, remoteFile: 'logflare.md', }, + { + slug: 'mongodb', + meta: { + title: 'MongoDB', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'mongodb.md', + }, { slug: 'mssql', meta: { @@ -334,14 +342,11 @@ interface Params { } const WrappersDocs = async (props: { params: Promise }) => { - const params = await props.params - if (!isFeatureEnabled('docs:fdw')) { - notFoundWithPathname( - `/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}` - ) + notFound() } + const params = await props.params const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params) // Create a combined URL transformer that handles both regular URLs and asset URLs diff --git a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx index 2a2cf31e917..4053787f65e 100644 --- a/apps/docs/app/guides/deployment/ci/[slug]/page.tsx +++ b/apps/docs/app/guides/deployment/ci/[slug]/page.tsx @@ -1,14 +1,15 @@ +import { notFound } from 'next/navigation' import { relative } from 'node:path' +import rehypeSlug from 'rehype-slug' + import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' -import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' +import { getGitHubFileContents } from '~/lib/octokit' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' -import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' -import rehypeSlug from 'rehype-slug' export const dynamicParams = false @@ -74,7 +75,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug) if (!page) { - notFoundWithPathname(`/guides/deployment/ci/${slug}`) + notFound() } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx index 2fafd8ea780..153c814eff3 100644 --- a/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/deployment/terraform/[[...slug]]/page.tsx @@ -1,6 +1,5 @@ import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { getEmptyArray } from '~/features/helpers.fn' import { IS_DEV } from '~/lib/constants' import { isValidGuideFrontmatter } from '~/lib/docs' @@ -11,6 +10,7 @@ import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import matter from 'gray-matter' +import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' import { @@ -106,7 +106,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find((page) => page.slug === requestedSlug) if (!page) { - notFoundWithPathname(`/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}`) + notFound() } const { meta, remoteFile, useRoot } = page diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx index 6e6bf5b3053..178f1f0dc37 100644 --- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx +++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import { GlassPanel } from 'ui-patterns' +import { GlassPanel } from 'ui-patterns/GlassPanel' import { getAiPrompts } from './AiPrompts.utils' diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index dda40f712a9..eb0c01b48f5 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -1,7 +1,6 @@ import { isAbsolute, relative } from 'path' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' -import { notFoundWithPathname } from '~/features/docs/notFound.utils' import { getEmptyArray } from '~/features/helpers.fn' import { IS_DEV } from '~/lib/constants' import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform' @@ -10,6 +9,7 @@ import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' import { getGitHubFileContents } from '~/lib/octokit' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' +import { notFound } from 'next/navigation' import rehypeSlug from 'rehype-slug' // We fetch these docs at build time from an external repo @@ -128,7 +128,7 @@ const getContent = async ({ slug }: Params) => { const page = pageMap.find((page) => page.slug === slug?.at(0)) if (!page) { - notFoundWithPathname(`/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}`) + notFound() } const { remoteFile, meta } = page diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx index f0868e8c167..9f15cac7d53 100644 --- a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx +++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx @@ -1,4 +1,5 @@ -import { notFoundWithPathname } from '~/features/docs/notFound.utils' +import { notFound } from 'next/navigation' + import TroubleshootingPage from '~/features/docs/Troubleshooting.page' import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils' import { PROD_URL } from '~/lib/constants' @@ -19,7 +20,7 @@ export default async function TroubleshootingEntryPage(props: { const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug) if (!entry) { - notFoundWithPathname(`/guides/troubleshooting/${slug}`) + notFound() } return diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index aea0eb429ee..e0a43504b95 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -4,6 +4,7 @@ import 'ui-patterns/ShimmeringLoader/index.css' import '../styles/globals.css' import '../styles/prism-okaidia.css' +import { SkipToContent } from '~/components/SkipToContent' import { GlobalProviders } from '~/features/app.providers' import { TopNavSkeleton } from '~/layouts/MainSkeleton' import { BASE_PATH, IS_PRODUCTION } from '~/lib/constants' @@ -12,6 +13,8 @@ import { TelemetryTagManager } from 'common' import { genFaviconData } from 'common/MetaFavicons/app-router' import type { Metadata, Viewport } from 'next' +import { inter, manrope } from '@/fonts' + const { metadataApplicationName, metadataTitle } = getCustomContent([ 'metadata:application_name', 'metadata:title', @@ -50,8 +53,9 @@ const viewport: Viewport = { const RootLayout = ({ children }: { children: React.ReactNode }) => { return ( - + + {children} 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 ( { + sendTelemetryEvent({ + action: 'docs_content_listing_clicked', + properties: { + targetPath: item.href, + linkTitle: item.title, + ...(groupLabel ? { groupTitle: groupLabel } : {}), + listingId: group.id, + }, + }) + }, + [sendTelemetryEvent, group.id, groupLabel] + ) + + return { trackClick } +} + +function ContentListingGroupHeading({ group }: { group: ContentListingGroup }) { + if (!group.heading) return null + + return {group.heading} +} + +function ContentListingsGroup({ group }: { group: ContentListingGroup }) { + const { trackClick } = useContentListingClickHandler(group) + const isGrid = group.type === 'grid' + const listClassName = isGrid ? 'grid md:grid-cols-12 gap-4' : 'list-disc pl-6 space-y-2' + const gridItemClassName = isGrid ? GRID_ITEM_CLASS[group.columns ?? 3] : undefined + + // Heading stays outside `not-prose` so it inherits the surrounding MDX prose + // typography. The list itself opts out so its explicit Tailwind layout wins. + return ( +
+ +
+ {group.description &&

{group.description}

} +
    + {group.items.map((item) => { + const external = isExternalContentListingHref(item.href) + const key = `${group.id}-${item.href}` + + if (isGrid) { + return ( +
  • + trackClick(item)} + target={external ? '_blank' : undefined} + > + + {item.description} + + +
  • + ) + } + + return ( +
  • + trackClick(item)} + target={external ? '_blank' : undefined} + > + {item.title}: {item.description} + +
  • + ) + })} +
+
+
+ ) +} + +export function ContentListings({ id }: { id: string }) { + const group = getContentListingById(id) + if (!group || !group.items.length) return null + + return ( +
+ +
+ ) +} diff --git a/apps/docs/components/ContentListings/index.tsx b/apps/docs/components/ContentListings/index.tsx new file mode 100644 index 00000000000..461a4ab891b --- /dev/null +++ b/apps/docs/components/ContentListings/index.tsx @@ -0,0 +1 @@ +export { ContentListings } from './ContentListings.client' diff --git a/apps/docs/components/CustomContent.tsx b/apps/docs/components/CustomContent.tsx new file mode 100644 index 00000000000..ed6c727b293 --- /dev/null +++ b/apps/docs/components/CustomContent.tsx @@ -0,0 +1,59 @@ +import { + getCustomContent, + type CustomContent as CustomContentKey, +} from '~/lib/custom-content/getCustomContent' +import type { ReactNode } from 'react' + +import { resolveSharedDataPath } from './SharedData.utils' + +type ValueFor = ReturnType< + typeof getCustomContent<[T]> +>[keyof ReturnType>] + +/** + * A wrapper component to access values from `custom-content.json` within MDX + * files. Mirrors the `getCustomContent` helper used in TSX code, and follows + * the same `data`/path-or-render-function pattern as `SharedData`. + * + * @param data - The `custom-content.json` key to read, e.g. `navigation:logo`. + * @param children - How to access the selected value. If it is a render + * function, it takes the value as a param. If it is a + * string, it takes a path through the value, formatted like + * `a[0].b.c`. If omitted, the value itself is rendered. + * + * @example Render a value inline + * + * + * @example Address a nested field with a path + * light + * + * @example Use a render function for richer output + * + * {(logo) => } + * + */ +function CustomContent({ + data, + children, +}: { + data: T + children?: ((value: ValueFor) => ReactNode) | string +}) { + const result = getCustomContent([data]) + const value = Object.values(result)[0] as ValueFor + + if (typeof children === 'function') { + return children(value) + } + if (typeof children === 'string') { + return resolveSharedDataPath(value, children) as ReactNode + } + + if (value != null && typeof value === 'object') { + return JSON.stringify(value) as unknown as ReactNode + } + + return (value ?? null) as ReactNode +} + +export { CustomContent } diff --git a/apps/docs/components/DocsCoverLogo.tsx b/apps/docs/components/DocsCoverLogo.tsx index 8a66b35ea13..3b56575513f 100644 --- a/apps/docs/components/DocsCoverLogo.tsx +++ b/apps/docs/components/DocsCoverLogo.tsx @@ -1,7 +1,7 @@ 'use client' +import { domAnimation, LazyMotion, m } from 'framer-motion' import React, { type PropsWithChildren } from 'react' -import { LazyMotion, domAnimation, m } from 'framer-motion' const DocsCoverLogo = (props: PropsWithChildren) => { const pathMotionConfig = { @@ -47,7 +47,7 @@ const DocsCoverLogo = (props: PropsWithChildren) => { animate={pathMotionConfig.animate} transition={pathMotionConfig.transition as any} d="M125.25 191.383C167.828 191.383 202.344 156.867 202.344 114.289C202.344 71.7115 167.828 37.1953 125.25 37.1953C82.6724 37.1953 48.1562 71.7115 48.1562 114.289C48.1562 156.867 82.6724 191.383 125.25 191.383Z" - stroke="hsl(var(--foreground-light))" + stroke="var(--foreground-light)" strokeOpacity="0.1" strokeWidth="0.7" strokeMiterlimit="10" @@ -215,7 +215,7 @@ const DocsCoverLogo = (props: PropsWithChildren) => { animate={pathMotionConfig.animate} transition={pathMotionConfig.transition as any} d="M218.024 169.846H32.498" - stroke="hsl(var(--foreground-light))" + stroke="var(--foreground-light)" strokeOpacity="0.1" strokeWidth="0.7" strokeMiterlimit="10" @@ -291,8 +291,8 @@ const DocsCoverLogo = (props: PropsWithChildren) => { gradientUnits="userSpaceOnUse" gradientTransform="translate(125.451 114.82) rotate(90) scale(109.781 0.5)" > - - + + { y2="11.9994" gradientUnits="userSpaceOnUse" > - - + + { y2="204.984" gradientUnits="userSpaceOnUse" > - - + + { y2="13.9993" gradientUnits="userSpaceOnUse" > - - + + { y2="204.984" gradientUnits="userSpaceOnUse" > - - + + { y2="-7.94661" gradientUnits="userSpaceOnUse" > - - - + + + { y2="-3.99932" gradientUnits="userSpaceOnUse" > - - - + + + { y2="199.061" gradientUnits="userSpaceOnUse" > - - - + + + { gradientUnits="userSpaceOnUse" gradientTransform="translate(125.752 114.291) rotate(90) scale(109.271 0.5)" > - - + + { y2="6.31757" gradientUnits="userSpaceOnUse" > - - + + { y2="200" gradientUnits="userSpaceOnUse" > - - + + { y2="199.061" gradientUnits="userSpaceOnUse" > - - - + + + { gradientUnits="userSpaceOnUse" gradientTransform="translate(125.261 59.2344) rotate(90) scale(0.5 263.749)" > - - + + { y2="94.9749" gradientUnits="userSpaceOnUse" > - - - + + + { y2="131.43" gradientUnits="userSpaceOnUse" > - - - + + + { y2="1.99785" gradientUnits="userSpaceOnUse" > - - - + + + 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 706ce54ca8d..f7973dbe089 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/McpCiConfigBlock.tsx b/apps/docs/components/McpCiConfigBlock.tsx new file mode 100644 index 00000000000..81e4c1d730d --- /dev/null +++ b/apps/docs/components/McpCiConfigBlock.tsx @@ -0,0 +1,16 @@ +import { CodeBlock } from '~/features/ui/CodeBlock/CodeBlock' +import { getCustomContent } from '~/lib/custom-content/getCustomContent' + +import { buildMcpCiConfig } from './McpCiConfigBlock.utils' + +/** + * Renders the example CI MCP server configuration with the remote MCP server + * URL pulled from `custom-content.json` (`mcp:servers`). Fenced code blocks in + * MDX render verbatim, so a dynamic value has to be injected via a component. + */ +export function McpCiConfigBlock() { + const { mcpServers } = getCustomContent(['mcp:servers']) + const config = buildMcpCiConfig(mcpServers?.remote) + + return +} diff --git a/apps/docs/components/McpCiConfigBlock.utils.ts b/apps/docs/components/McpCiConfigBlock.utils.ts new file mode 100644 index 00000000000..d13c7274d6d --- /dev/null +++ b/apps/docs/components/McpCiConfigBlock.utils.ts @@ -0,0 +1,19 @@ +/** + * Builds the example CI MCP server configuration. Pure and dependency-free so + * it can be shared between the React `` component (Next.js + * bundle) and the build-time markdown-schema handler without pulling in + * `CodeBlock`'s heavier dependencies (Shiki, Twoslash) into the build script. + */ +export function buildMcpCiConfig(remoteUrl = 'https://mcp.supabase.com/mcp') { + return { + mcpServers: { + supabase: { + type: 'http', + url: `${remoteUrl}?project_ref=\${SUPABASE_PROJECT_REF}`, + headers: { + Authorization: 'Bearer ${SUPABASE_ACCESS_TOKEN}', + }, + }, + }, + } +} diff --git a/apps/docs/components/Mermaid.tsx b/apps/docs/components/Mermaid.tsx index 1ffbf346c4c..4b018558c0c 100644 --- a/apps/docs/components/Mermaid.tsx +++ b/apps/docs/components/Mermaid.tsx @@ -6,13 +6,13 @@ interface MermaidProps { export function Mermaid({ chart }: MermaidProps) { const svg = renderMermaidSVG(chart, { - bg: 'hsl(var(--background-default))', - fg: 'hsl(var(--foreground-default))', + bg: 'var(--background-default)', + fg: 'var(--foreground-default)', accent: 'hsl(var(--brand-default))', - muted: 'hsl(var(--foreground-light))', - line: 'hsl(var(--border-strong))', - border: 'hsl(var(--border-strong))', - surface: 'hsl(var(--background-surface-200))', + muted: 'var(--foreground-light)', + line: 'var(--border-strong)', + border: 'var(--border-strong)', + surface: 'var(--background-surface-200)', transparent: true, }) 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/Navigation.types.ts b/apps/docs/components/Navigation/Navigation.types.ts index b786a96586e..9dfc85e6142 100644 --- a/apps/docs/components/Navigation/Navigation.types.ts +++ b/apps/docs/components/Navigation/Navigation.types.ts @@ -24,6 +24,7 @@ type MenuItem = { level?: string hasLightIcon?: boolean community?: boolean + new?: boolean enabled?: boolean } diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx index 82a6bf3a196..e08325e5538 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx @@ -63,6 +63,7 @@ const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => { href={item.href} title={item.label} community={item.community} + new={item.new} icon={item.icon} /> ) @@ -162,7 +163,7 @@ const GlobalMobileMenu = ({ open, setOpen }: Props) => { ) : ( <> -
@@ -445,7 +453,7 @@ function LoginHint({ variable }: { variable: Variable }) { if (isUserLoading || isLoggedIn) return null return ( -

+

To get your {prettyFormatVariable[variable]},{' '} = Object.fromEntries( + COMPUTE_OPTIONS.map((o) => [o.value, o.label]) +) + +// Input-parameter columns (only shown in the full/raw table). +export const THROUGHPUT_PARAM_HEADINGS = ['RLS', 'Connected clients'] as const + +// Result columns (shown in both the current-selection table and the raw table). +export const THROUGHPUT_METRIC_HEADINGS = [ + 'Total DB changes /sec', + 'Max messages per client /sec', + 'Max total messages /sec', + 'Latency p95', +] as const + +export const THROUGHPUT_TABLE_HEADINGS = [ + ...THROUGHPUT_PARAM_HEADINGS, + ...THROUGHPUT_METRIC_HEADINGS, +] as const diff --git a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx index cef1f172a2b..7f13c8bb837 100644 --- a/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx +++ b/apps/docs/components/RealtimeLimitsEstimator/RealtimeLimitsEstimator.tsx @@ -14,50 +14,45 @@ import { SelectValue, } from 'ui' +import { + COMPUTE_LABELS, + COMPUTE_OPTIONS, + THROUGHPUT_METRIC_HEADINGS, + THROUGHPUT_TABLE_HEADINGS, +} from './RealtimeLimitsEstimator.constants' + export default function RealtimeLimitsEstimater({}) { - const findTableValue = ({ computeAddOn, filters, rls, concurrency }) => { + const findTableValue = ({ computeAddOn, rls, concurrency }) => { return throughputTable.find( - (l) => - l.computeAddOn === computeAddOn && - l.filters === filters && - l.rls === rls && - l.concurrency === concurrency + (l) => l.computeAddOn === computeAddOn && l.rls === rls && l.concurrency === concurrency ) } const [computeAddOn, setComputeAddOn] = useState('micro') - const [filters, setFilters] = useState(false) const [rls, setRLS] = useState(false) const [concurrency, setConcurrency] = useState(500) - const [limits, setLimits] = useState(findTableValue({ computeAddOn, filters, rls, concurrency })) + const [limits, setLimits] = useState(findTableValue({ computeAddOn, rls, concurrency })) const [expandPreview, setExpandPreview] = useState(false) const handleComputeAddOnSelection = (val) => { setComputeAddOn(val) setConcurrency(500) - setLimits(findTableValue({ computeAddOn: val, filters, rls, concurrency: 500 })) - } - - const handleFiltersSelection = (value) => { - const val = value.toLowerCase() === 'true' - setFilters(val) - setConcurrency(500) - setLimits(findTableValue({ computeAddOn, filters: val, rls, concurrency: 500 })) + setLimits(findTableValue({ computeAddOn: val, rls, concurrency: 500 })) } const handleRLSSelection = (value) => { const val = value.toLowerCase() === 'true' setRLS(val) setConcurrency(500) - setLimits(findTableValue({ computeAddOn, filters, rls: val, concurrency: 500 })) + setLimits(findTableValue({ computeAddOn, rls: val, concurrency: 500 })) } const handleConcurrencySelection = (value) => { const val = parseInt(value) setConcurrency(val) - setLimits(findTableValue({ computeAddOn, filters, rls, concurrency: val })) + setLimits(findTableValue({ computeAddOn, rls, concurrency: val })) } return ( @@ -71,21 +66,11 @@ export default function RealtimeLimitsEstimater({}) { - Micro - Small to medium - Large to 16XL - - -

-
- -
@@ -109,9 +94,7 @@ export default function RealtimeLimitsEstimater({}) { {throughputTable - .filter( - (l) => l.computeAddOn === computeAddOn && l.filters === filters && l.rls === rls - ) + .filter((l) => l.computeAddOn === computeAddOn && l.rls === rls) .map((l) => ( {Intl.NumberFormat().format(l.concurrency)} @@ -129,10 +112,11 @@ export default function RealtimeLimitsEstimater({}) { - - - - + {THROUGHPUT_METRIC_HEADINGS.map((heading) => ( + + ))} @@ -154,7 +138,7 @@ export default function RealtimeLimitsEstimater({}) {

View raw throughput table

Total DB changes /secMax messages per client /secMax total messages /secLatency p95 + {heading} +
- - - - - - - + {THROUGHPUT_TABLE_HEADINGS.map((heading) => ( + + ))} @@ -198,7 +174,6 @@ export default function RealtimeLimitsEstimater({}) { .filter((l) => l.computeAddOn === computeAddOn) .map((l) => ( -
FiltersRLSConnected clientsTotal DB changes /secMax messages per client /secMax total messages /secLatency p95 + {heading} +
{l.filters ? '✅' : '🚫'} {l.rls ? '✅' : '🚫'} {Intl.NumberFormat().format(l.concurrency)} diff --git a/apps/docs/components/SharedData.tsx b/apps/docs/components/SharedData.tsx index 113a8340195..744be85b829 100644 --- a/apps/docs/components/SharedData.tsx +++ b/apps/docs/components/SharedData.tsx @@ -1,7 +1,8 @@ -import { at } from 'lodash-es' import { ReactNode } from 'react' import { config, logConstants } from 'shared-data' +import { resolveSharedDataPath } from './SharedData.utils' + const sharedData = { config, logConstants, @@ -25,12 +26,10 @@ function SharedData({ data: keyof typeof sharedData children: ((selectedData: (typeof sharedData)[keyof typeof sharedData]) => ReactNode) | string }) { - let selectedData = sharedData[data] as any - return typeof children === 'string' - ? ((typeof (selectedData = at(selectedData, [children])[0]) === 'object' - ? `${selectedData.value ?? ''} ${selectedData.unit ?? ''}`.trim() - : selectedData) as unknown as ReactNode) - : children(selectedData) + if (typeof children === 'string') { + return resolveSharedDataPath(sharedData[data], children) as ReactNode + } + return children(sharedData[data]) } export { SharedData } diff --git a/apps/docs/components/SharedData.utils.ts b/apps/docs/components/SharedData.utils.ts new file mode 100644 index 00000000000..512eab59cfe --- /dev/null +++ b/apps/docs/components/SharedData.utils.ts @@ -0,0 +1,19 @@ +import { at } from 'lodash-es' + +/** + * Resolves a dot/bracket path within a shared-data dataset. If the resolved + * value is an object with `value`/`unit` fields, returns `${value} ${unit}` + * (trimmed); otherwise returns the resolved primitive as-is. + * + * Pure: no `shared-data` import. Callers supply the dataset so this util can + * be reused by the React `` component (Next.js bundle) and by the + * build-time markdown-schema handler (tsx) without each having to navigate + * `shared-data`'s ESM/CJS interop independently. + */ +export function resolveSharedDataPath(dataset: unknown, path: string): string | number | undefined { + const selected = at(dataset as any, [path])[0] + if (typeof selected === 'object' && selected !== null) { + return `${(selected as any).value ?? ''} ${(selected as any).unit ?? ''}`.trim() + } + return selected +} diff --git a/apps/docs/components/SkipToContent.tsx b/apps/docs/components/SkipToContent.tsx new file mode 100644 index 00000000000..8b31bcb2243 --- /dev/null +++ b/apps/docs/components/SkipToContent.tsx @@ -0,0 +1,22 @@ +import { DOCS_CONTENT_CONTAINER_ID } from '~/features/ui/helpers.constants' +import Link from 'next/link' +import { Button, cn } from 'ui' + +const SkipToContent = () => { + return ( + + ) +} + +export { SkipToContent } diff --git a/apps/docs/components/StepHikeCompact/index.tsx b/apps/docs/components/StepHikeCompact/index.tsx index a20e5f8d4bb..ef1a7f08af7 100644 --- a/apps/docs/components/StepHikeCompact/index.tsx +++ b/apps/docs/components/StepHikeCompact/index.tsx @@ -72,22 +72,46 @@ const Step: FC> = ({ children, title, step }) => { -
{children}
+
+ {children} +
) } const Details: FC> = ({ children, title, fullWidth = false }) => { return ( -
-

{title}

+
+ {title &&

{title}

} {children}
) } const Code: FC> = ({ children }) => { - return
{children}
+ return ( +
+ {children} +
+ ) } StepHikeCompact.Step = Step diff --git a/apps/docs/content/_partials/ai/quickstart_hf_deployment.mdx b/apps/docs/content/_partials/ai/quickstart_hf_deployment.mdx index 595b1f8f25b..5cd2f61f7aa 100644 --- a/apps/docs/content/_partials/ai/quickstart_hf_deployment.mdx +++ b/apps/docs/content/_partials/ai/quickstart_hf_deployment.mdx @@ -2,4 +2,4 @@ If you have your own infrastructure for deploying Python apps, you can continue to use `vecs` as described in this guide. -Alternatively if you would like to quickly deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript. +Alternatively if you would like to deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript. diff --git a/apps/docs/content/_partials/api_rate_limits.mdx b/apps/docs/content/_partials/api_rate_limits.mdx index e72382fd1c6..c050a467f80 100644 --- a/apps/docs/content/_partials/api_rate_limits.mdx +++ b/apps/docs/content/_partials/api_rate_limits.mdx @@ -57,6 +57,12 @@ Some endpoints have stricter rate limits than the standard 120 requests per minu **Note:** The `GET /v1/projects/:ref/database/context` endpoint has dual rate limiting. You can make up to 10 requests per minute, but also no more than 1 request per second to prevent burst traffic. +Some endpoints have the standard 120 requests per minute but with different timeout durations: + +| Endpoint | Limit | Duration | Reason | +| -------------------------------------------- | ------------ | --------- | ---------------------------------------------------- | +| `POST /v1/projects/:ref/database/migrations` | 120 requests | 3 minutes | Database migrations may require more processing time | + ### Best practices - **Monitor rate limit headers** - Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit. diff --git a/apps/docs/content/_partials/api_settings.mdx b/apps/docs/content/_partials/api_settings.mdx index b60a575b2e5..15da66146f3 100644 --- a/apps/docs/content/_partials/api_settings.mdx +++ b/apps/docs/content/_partials/api_settings.mdx @@ -1,10 +1,12 @@ ### Get API details -Now that you've created some database tables, you are ready to insert data using the auto-generated API. +To interact with data in database tables, you use the client libraries that wrap [the auto-generated Data API endpoints](/docs/guides/api), authenticating using the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}). -To do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}). + + -[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses. + -<$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, their uses, and where to find them. + + diff --git a/apps/docs/content/_partials/api_settings_steps.mdx b/apps/docs/content/_partials/api_settings_steps.mdx deleted file mode 100644 index 19b199f746b..00000000000 --- a/apps/docs/content/_partials/api_settings_steps.mdx +++ /dev/null @@ -1,12 +0,0 @@ -{/* TODO: How to completely consolidate partials? */} - -### Get API details - -Now that you've created some database tables, you are ready to insert data using the auto-generated API. - -To do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}). - -[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses. - -<$Partial path="api_keys_deprecation.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }} -/> diff --git a/apps/docs/content/_partials/cost_warning.mdx b/apps/docs/content/_partials/cost_warning.mdx new file mode 100644 index 00000000000..6f8c536f5bb --- /dev/null +++ b/apps/docs/content/_partials/cost_warning.mdx @@ -0,0 +1,7 @@ + + +To keep SMS sending costs under control, make sure you adjust your project's rate limits and [configure CAPTCHA](/docs/guides/auth/auth-captcha). See the [Production Checklist](/docs/guides/platform/going-into-prod) to learn more. + +Some countries have special regulations for services that send SMS messages to users, (e.g India's TRAI DLT regulations). Remember to look up and follow the regulations of countries where you operate. + + diff --git a/apps/docs/content/_partials/database_setup.mdx b/apps/docs/content/_partials/database_setup.mdx index 92112438ddc..09cac33af3c 100644 --- a/apps/docs/content/_partials/database_setup.mdx +++ b/apps/docs/content/_partials/database_setup.mdx @@ -1,6 +1,6 @@ ## Project setup -Let's create a new Postgres database. This is as simple as starting a new Project in Supabase: +To create a new Postgres database, start a new Project in Supabase: 1. [Create a new project](https://database.new/) in the Supabase dashboard. 1. Enter your project details. Remember to store your password somewhere safe. diff --git a/apps/docs/content/_partials/db_pre_request_warning.mdx b/apps/docs/content/_partials/db_pre_request_warning.mdx index 0b61bc21917..5bf6a4fc56a 100644 --- a/apps/docs/content/_partials/db_pre_request_warning.mdx +++ b/apps/docs/content/_partials/db_pre_request_warning.mdx @@ -22,7 +22,7 @@ on todos for select using ( set_information() AND (select auth.uid()) = user_id ); ``` -This ensures the function is called when evaluating RLS policies for all products, not just Data API requests. +This ensures the function is called when evaluating RLS policies for all products, not only Data API requests. **Performance consideration:** diff --git a/apps/docs/content/_partials/kotlin_project_setup.mdx b/apps/docs/content/_partials/kotlin_project_setup.mdx index 1f32521eefd..546c23d80bb 100644 --- a/apps/docs/content/_partials/kotlin_project_setup.mdx +++ b/apps/docs/content/_partials/kotlin_project_setup.mdx @@ -1,6 +1,6 @@ ## Project setup -Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database. +Before building, you must set up your Database and API with a new Project in Supabase and then create a "schema" inside the database. ### Create a project @@ -10,7 +10,7 @@ Before we start building we're going to set up our Database and API. This is as ### Set up the database schema -Now we are going to set up the database schema. You can just copy/paste the SQL from below and run it yourself. +Now we are going to set up the database schema. You can copy/paste the SQL from below and run it yourself. What you can do with the Metrics API} + header="What you can do with the Metrics API" id="how-do-i-check-when-a-user-went-through-mfa" className="border-0 px-2 py-4" > @@ -25,7 +24,7 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground - **Username**: `service_role` - **Password**: a **Secret API key** (`sb_secret_...`). You can create/copy it in [**Project Settings → API Keys**](/dashboard/project/_/settings/api-keys). For more context, see [Understanding API keys](/docs/guides/getting-started/api-keys). - Testing locally is as simple as running `curl` with your Secret API key: + To test locally, run `curl` with your Secret API key: ```bash curl /customer/v1/privileged/metrics \ diff --git a/apps/docs/content/_partials/postgres_installation.mdx b/apps/docs/content/_partials/postgres_installation.mdx index a116934ecdd..876b02094f3 100644 --- a/apps/docs/content/_partials/postgres_installation.mdx +++ b/apps/docs/content/_partials/postgres_installation.mdx @@ -25,7 +25,7 @@ Add the Postgres binary to your system PATH. - In Control Panel, under the Advanced tab of System Properties, click Environment Variables. Edit the Path variable by adding the path to the SQL binary you just installed. + In Control Panel, under the Advanced tab of System Properties, click Environment Variables. Edit the Path variable by adding the path to the SQL binary you installed. The path will look something like this, though it may differ slightly depending on your installed version: diff --git a/apps/docs/content/_partials/quickstart_db_setup.mdx b/apps/docs/content/_partials/quickstart_db_setup.mdx index eedf4167677..08e577f4446 100644 --- a/apps/docs/content/_partials/quickstart_db_setup.mdx +++ b/apps/docs/content/_partials/quickstart_db_setup.mdx @@ -1,84 +1,87 @@ - +export const sqlSetup = `-- Create the table +create table instruments ( + id bigint primary key generated always as identity, + name text not null +); -Go to [database.new](https://database.new) and create a new Supabase project. +-- Insert sample data into the table +insert into instruments (name) +values +('violin'), +('viola'), +('cello'); -Alternatively, you can create a project using the Management API: +-- Grant the privileges the role needs, which is read access +grant select on public.instruments to anon; - +-- Enable row level security for the table +alter table instruments enable row level security; - +-- Create a policy to allow the anon role to read from the instruments table +create policy "public can read instruments" +on public.instruments +for select to anon +using (true);` -```bash -# First, get your access token from https://supabase.com/dashboard/account/tokens -export SUPABASE_ACCESS_TOKEN="your-access-token" +## 1. Create a Supabase project -# List your organizations to get the organization ID -curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ - https://api.supabase.com/v1/organizations +To start, you need a Supabase project. -# Create a new project (replace with your organization ID) -curl -X POST https://api.supabase.com/v1/projects \ - -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "organization_id": "", - "name": "My Project", - "region": "us-east-1", - "db_pass": "" - }' +Create a new Supabase project from [the Dashboard of any organization](/dashboard/new/_) you belong to. + + + +Use [the Management API](/docs/reference/api/v1-create-a-project) or ask [the MCP server](/docs/guides/ai-tools/mcp#account-management) to create a new Supabase project. + + + +## 2. Set up your database + +When your Supabase project is up and running, create an `instruments` table with some sample data. Then set only the privileges each Postgres role needs, add [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for enhanced security for database data by default, and create an RLS policy to make the data in the table publicly readable. + +Do these steps within your project's dashboard by copying and running the snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). + + + +Save some steps by clicking here to prefill the SQL in the SQL Editor, and then clicking **Run**. + + + + + +You can use [the Management API](/docs/reference/api/v1-run-a-query) or ask [the MCP server](/docs/guides/ai-tools/mcp#database) to execute SQL queries. + + + +```sql SQL_EDITOR +-- Create the table +create table instruments ( + id bigint primary key generated always as identity, + name text not null +); + +-- Insert sample data into the table +insert into instruments (name) +values + ('violin'), + ('viola'), + ('cello'); + +-- Grant the privileges the role needs, which is read access +grant select on public.instruments to anon; + +-- Enable row level security for the table +alter table instruments enable row level security; + +-- Create a policy to allow the anon role to read from the instruments table +create policy "public can read instruments" +on public.instruments +for select to anon +using (true); ``` - + - +If you disabled the Data API during project setup, enable it in the [**Integrations > Data API**](/dashboard/project/_/integrations/data_api/settings) section of the Dashboard and expose the specific tables or functions you want to access. To automatically grant access for new tables and functions in `public`, enable **Automatically expose new tables**. -When your project is up and running, go to the [**Table Editor**](/dashboard/project/_/editor) section of the Dashboard, create a new table and insert some data. Then in the [**Integrations > Data API**](/dashboard/project/_/integrations/data_api/settings) section of the Dashboard, expose the specific tables or functions you want to access. To automatically grant access for new tables and functions in `public`, enable **Default privileges for new entities**. - -Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). - -This creates an `instruments` table with some sample data, sets a secure baseline by setting only the privileges each Postgres role needs, and adds [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for enhanced security for database data by default. - - - - - - ```sql SQL_EDITOR - -- Create the table - create table instruments ( - id bigint primary key generated always as identity, - name text not null - ); - - -- Insert sample data into the table - insert into instruments (name) - values - ('violin'), - ('viola'), - ('cello'); - - -- Grant the privileges the role needs, which is read access - grant select on public.instruments to anon; - - -- Enable row level security for the table - alter table instruments enable row level security; - ``` - - - - - -Create an RLS policy to make the data in your table publicly readable: - - - - - - ```sql SQL_EDITOR - -- Create a policy to allow the anon role to read from the instruments table - create policy "public can read instruments" - on public.instruments - for select to anon - using (true); - ``` - - + diff --git a/apps/docs/content/_partials/uiLibCta.mdx b/apps/docs/content/_partials/uiLibCta.mdx index befedb97925..8973997dfc9 100644 --- a/apps/docs/content/_partials/uiLibCta.mdx +++ b/apps/docs/content/_partials/uiLibCta.mdx @@ -2,7 +2,7 @@ UI components built on shadcn/ui that connect to Supabase via a single command. - diff --git a/apps/docs/content/errorCodes/authErrorCodes.toml b/apps/docs/content/errorCodes/authErrorCodes.toml deleted file mode 100644 index 8d33253b3e0..00000000000 --- a/apps/docs/content/errorCodes/authErrorCodes.toml +++ /dev/null @@ -1,283 +0,0 @@ -# Official error codes for Supabase Auth -# -# Error codes should be documented in the following format -# -# [error_code] -# description = "Error description." -# resolution = "How to resolve this error." -# [[error_code.references]] -# href = "https://supabase.com/docs/some/relevant/guide" -# description = "Guide for doing some relevant thing" -# -# error_code should be a unique and stable identifier for the error, that the -# developer can match against for error handling. - -[anonymous_provider_disabled] -description = "Anonymous sign-ins are disabled." - -[bad_code_verifier] -description = "Returned from the PKCE flow where the provided code verifier does not match the expected one. Indicates a bug in the implementation of the client library." - -[bad_json] -description = "Usually used when the HTTP body of the request is not valid JSON." - -[bad_jwt] -description = "JWT sent in the Authorization header is not valid." - -[bad_oauth_callback] -description = "OAuth callback from provider to Auth does not have all the required attributes (state). Indicates an issue with the OAuth provider or client library implementation." - -[bad_oauth_state] -description = "OAuth state (data echoed back by the OAuth provider to Supabase Auth) is not in the correct format. Indicates an issue with the OAuth provider integration." - -[captcha_failed] -description = "CAPTCHA challenge could not be verified with the CAPTCHA provider. Check your CAPTCHA integration." - -[conflict] -description = "General database conflict, such as concurrent requests on resources that should not be modified concurrently. Can often occur when you have too many session refresh requests firing off at the same time for a user. Check your app for concurrency issues, and if detected, back off exponentially." - -[email_address_invalid] -description = "Example and test domains are currently not supported. Use a different email address." - -[email_address_not_authorized] -description = "Email sending is not allowed for this address as your project is using the default SMTP service. Emails can only be sent to members in your Supabase organization. If you want to send emails to others, set up a custom SMTP provider." -[[email_address_not_authorized.references]] -href = "https://supabase.com/docs/guides/auth/auth-smtp" -description = "Setting up a custom SMTP provider" - -[email_conflict_identity_not_deletable] -description = "Unlinking this identity causes the user's account to change to an email address which is already used by another user account. Indicates an issue where the user has two different accounts using different primary email addresses. You may need to migrate user data to one of their accounts in this case." - -[email_exists] -description = "Email address already exists in the system." - -[email_not_confirmed] -description = "Signing in is not allowed for this user as the email address is not confirmed." - -[email_provider_disabled] -description = "Signups are disabled for email and password." - -[flow_state_expired] -description = "PKCE flow state to which the API request relates has expired. Ask the user to sign in again." - -[flow_state_not_found] -description = "PKCE flow state to which the API request relates no longer exists. Flow states expire after a while and are progressively cleaned up, which can cause this error. Retried requests can cause this error, as the previous request likely destroyed the flow state. Ask the user to sign in again." - -[hook_payload_invalid_content_type] -description = "Payload from Auth does not have a valid Content-Type header." - -[hook_payload_over_size_limit] -description = "Payload from Auth exceeds maximum size limit." - -[hook_timeout] -description = "Unable to reach hook within maximum time allocated." - -[hook_timeout_after_retry] -description = "Unable to reach hook after maximum number of retries." - -[identity_already_exists] -description = "The identity to which the API relates is already linked to a user." - -[identity_not_found] -description = "Identity to which the API call relates does not exist, such as when an identity is unlinked or deleted." - -[insufficient_aal] -description = "To call this API, the user must have a higher Authenticator Assurance Level. To resolve, ask the user to solve an MFA challenge." -[[insufficient_aal.references]] -href = "https://supabase.com/docs/guides/auth/auth-mfa" -description = "MFA" - -[invite_not_found] -description = "Invite is expired or already used." - -[invalid_credentials] -description = "Login credentials or grant type not recognized." - -[manual_linking_disabled] -description = "Calling the supabase.auth.linkUser() and related APIs is not enabled on the Auth server." - -[mfa_challenge_expired] -description = "Responding to an MFA challenge should happen within a fixed time period. Request a new challenge when encountering this error." - -[mfa_factor_name_conflict] -description = "MFA factors for a single user should not have the same friendly name." - -[mfa_factor_not_found] -description = "MFA factor no longer exists." - -[mfa_ip_address_mismatch] -description = "The enrollment process for MFA factors must begin and end with the same IP address." - -[mfa_phone_enroll_not_enabled] -description = "Enrollment of MFA Phone factors is disabled." - -[mfa_phone_verify_not_enabled] -description = "Login via Phone factors and verification of new Phone factors is disabled." - -[mfa_totp_enroll_not_enabled] -description = "Enrollment of MFA TOTP factors is disabled." - -[mfa_totp_verify_not_enabled] -description = "Login via TOTP factors and verification of new TOTP factors is disabled." - -[mfa_verification_failed] -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" -description = "MFA verification hook" - -[mfa_verified_factor_exists] -description = "Verified phone factor already exists for a user. Unenroll existing verified phone factor to continue." - -[mfa_web_authn_enroll_not_enabled] -description = "Enrollment of MFA Web Authn factors is disabled." - -[mfa_web_authn_verify_not_enabled] -description = "Login via WebAuthn factors and verification of new WebAuthn factors is disabled." - -[no_authorization] -description = "This HTTP request requires an Authorization header, which is not provided." - -[not_admin] -description = "User accessing the API is not admin, i.e. the JWT does not contain a role claim that identifies them as an admin of the Auth server." - -[oauth_provider_not_supported] -description = "Using an OAuth provider which is disabled on the Auth server." - -[otp_disabled] -description = "Sign in with OTPs (magic link, email OTP) is disabled. Check your server's configuration." - -[otp_expired] -description = "OTP code for this sign-in has expired. Ask the user to sign in again." - -[over_email_send_rate_limit] -description = "Too many emails have been sent to this email address. Ask the user to wait a while before trying again." - -[over_request_rate_limit] -description = "Too many requests have been sent by this client (IP address). Ask the user to try again in a few minutes. Sometimes can indicate a bug in your application that mistakenly sends out too many requests (such as a badly written useEffect React hook)." -[[over_request_rate_limit.references]] -href = "https://react.dev/reference/react/useEffect" -description = "React useEffect hook" - -[over_sms_send_rate_limit] -description = "Too many SMS messages have been sent to this phone number. Ask the user to wait a while before trying again." - -[phone_exists] -description = "Phone number already exists in the system." - -[phone_not_confirmed] -description = "Signing in is not allowed for this user as the phone number is not confirmed." - -[phone_provider_disabled] -description = "Signups are disabled for phone and password." - -[provider_disabled] -description = "OAuth provider is disabled for use. Check your server's configuration." - -[provider_email_needs_verification] -description = "Not all OAuth providers verify their user's email address. Supabase Auth requires emails to be verified, so this error is sent out when a verification email is sent after completing the OAuth flow." - -[reauthentication_needed] -description = "A user needs to reauthenticate to change their password. Ask the user to reauthenticate by calling the supabase.auth.reauthenticate() API." - -[reauthentication_not_valid] -description = "Verifying a reauthentication failed, the code is incorrect. Ask the user to enter a new code." - -[refresh_token_not_found] -description = "Session containing the refresh token not found." - -[refresh_token_already_used] -description = "Refresh token has been revoked and falls outside the refresh token reuse interval. See the documentation on sessions for further information." -[[refresh_token_already_used.references]] -href = "https://supabase.com/docs/guides/auth/sessions" -description = "Auth sessions" - -[request_timeout] -description = "Processing the request took too long. Retry the request." - -[same_password] -description = "A user that is updating their password must use a different password than the one currently used." - -[saml_assertion_no_email] -description = "SAML assertion (user information) was received after sign in, but no email address was found in it, which is required. Check the provider's attribute mapping and/or configuration." - -[saml_assertion_no_user_id] -description = "SAML assertion (user information) was received after sign in, but a user ID (called NameID) was not found in it, which is required. Check the SAML identity provider's configuration." - -[saml_entity_id_mismatch] -description = "(Admin API.) Updating the SAML metadata for a SAML identity provider is not possible, as the entity ID in the update does not match the entity ID in the database. This is equivalent to creating a new identity provider, and you should do that instead." - -[saml_idp_already_exists] -description = "(Admin API.) Adding a SAML identity provider that is already added." - -[saml_idp_not_found] -description = "SAML identity provider not found. Most often returned after IdP-initiated sign-in with an unregistered SAML identity provider in Supabase Auth." - -[saml_metadata_fetch_failed] -description = "(Admin API.) Adding or updating a SAML provider failed as its metadata could not be fetched from the provided URL." - -[saml_provider_disabled] -description = "Using Enterprise SSO with SAML 2.0 is not enabled on the Auth server." -[[saml_provider_disabled.references]] -href = "https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml" -description = "Enterprise SSO" - -[saml_relay_state_expired] -description = "SAML relay state is an object that tracks the progress of a supabase.auth.signInWithSSO() request. The SAML identity provider should respond after a fixed amount of time, after which this error is shown. Ask the user to sign in again." - -[saml_relay_state_not_found] -description = "SAML relay states are progressively cleaned up after they expire, which can cause this error. Ask the user to sign in again." - -[session_expired] -description = "Session to which the API request relates has expired. This can occur if an inactivity timeout is configured, or the session entry has exceeded the configured timebox value. See the documentation on sessions for more information." -[[session_expired.references]] -href = "https://supabase.com/docs/guides/auth/sessions" -description = "Auth sessions" - -[session_not_found] -description = "Session to which the API request relates no longer exists. This can occur if the user has signed out, or the session entry in the database was deleted in some other way." - -[signup_disabled] -description = "Sign ups (new account creation) are disabled on the server." - -[single_identity_not_deletable] -description = "Every user must have at least one identity attached to it, so deleting (unlinking) an identity is not allowed if it's the only one for the user." - -[sms_send_failed] -description = "Sending an SMS message failed. Check your SMS provider configuration." - -[sso_domain_already_exists] -description = "(Admin API.) Only one SSO domain can be registered per SSO identity provider." - -[sso_provider_not_found] -description = "SSO provider not found. Check the arguments in supabase.auth.signInWithSSO()." - -[too_many_enrolled_mfa_factors] -description = "A user can only have a fixed number of enrolled MFA factors." - -[unexpected_audience] -description = "(Deprecated feature not available via Supabase client libraries.) The request's X-JWT-AUD claim does not match the JWT's audience." - -[unexpected_failure] -description = "Auth service is degraded or a bug is present, without a specific reason." - -[user_already_exists] -description = "User with this information (email address, phone number) cannot be created again as it already exists." - -[user_banned] -description = "User to which the API request relates has a banned_until property which is still active. No further API requests should be attempted until this field is cleared." - -[user_not_found] -description = "User to which the API request relates no longer exists." - -[user_sso_managed] -description = "When a user comes from SSO, certain fields of the user cannot be updated (like email)." - -[validation_failed] -description = "Provided parameters are not in the expected format." - -[weak_password] -description = "User is signing up or changing their password without meeting the password strength criteria. Use the AuthWeakPasswordError class to access more information about what they need to do to make the password pass." diff --git a/apps/docs/content/errorCodes/realtimeErrorCodes.toml b/apps/docs/content/errorCodes/realtimeErrorCodes.toml deleted file mode 100644 index 57b5f8f5e60..00000000000 --- a/apps/docs/content/errorCodes/realtimeErrorCodes.toml +++ /dev/null @@ -1,215 +0,0 @@ -# Official error codes for Supabase Realtime -# -# Error codes should be documented in the following format -# -# [error_code] -# description = "Error description." -# resolution = "How to resolve this error." -# [[error_code.references]] -# href = "https://supabase.com/docs/some/relevant/guide" -# description = "Guide for doing some relevant thing" -# -# error_code should be a unique and stable identifier for the error, that the -# developer can match against for error handling. - -[TopicNameRequired] -description = "You are trying to use Realtime without a topic name set." - -[RealtimeDisabledForConfiguration] -description = "The configuration provided to Realtime on connect will not be able to provide you any Postgres Changes." -resolution = "Verify your configuration on channel startup as you might not have your tables properly registered." - -[TenantNotFound] -description = "The tenant you are trying to connect to does not exist." -resolution = "Verify the tenant name you are trying to connect to exists in the realtime.tenants table." - -[ErrorConnectingToWebsocket] -description = "Error when trying to connect to the WebSocket server." -resolution = "Verify user information on connect." - -[ErrorAuthorizingWebsocket] -description = "Error when trying to authorize the WebSocket connection." -resolution = "Verify user information on connect." - -[TableHasSpacesInName] -description = "The table you are trying to listen to has spaces in its name which we are unable to support." -resolution = "Change the table name to not have spaces in it." - -[UnableToDeleteTenant] -description = "Error when trying to delete a tenant." - -[UnableToSetPolicies] -description = "Error when setting up Authorization Policies." - -[UnableCheckoutConnection] -description = "Error when trying to checkout a connection from the tenant pool." - -[UnableToSubscribeToPostgres] -description = "Error when trying to subscribe to Postgres changes." - -[ReconnectSubscribeToPostgres] -description = "Postgres changes still waiting to be subscribed." - -[ChannelRateLimitReached] -description = "The number of channels you can create has reached its limit." - -[ConnectionRateLimitReached] -description = "The number of connected clients has reached its limit." - -[ClientJoinRateLimitReached] -description = "The rate of joins per second from your clients has reached the channel limits." - -[RealtimeDisabledForTenant] -description = "Realtime has been disabled for the tenant." -resolution = "Your project may have been suspended for exceeding usage quotas. Contact support with your project reference ID and a description of your Realtime use case." -[[RealtimeDisabledForTenant.references]] -href = "https://supabase.com/docs/troubleshooting/realtime-project-suspended-for-exceeding-quotas" -description = "Troubleshooting guide for suspended projects" - -[UnableToConnectToTenantDatabase] -description = "Realtime was not able to connect to the tenant's database." - -[DatabaseLackOfConnections] -description = "Realtime was not able to connect to the tenant's database due to not having enough available connections." -resolution = "Verify your database connection limits." -[[DatabaseLackOfConnections.references]] -href = "https://supabase.com/docs/guides/database/connection-management" -description = "Connection management guide" - -[RealtimeNodeDisconnected] -description = "Realtime is a distributed application and this means that one the system is unable to communicate with one of the distributed nodes." - -[MigrationsFailedToRun] -description = "Error when running the migrations against the Tenant database that are required by Realtime." - -[StartListenAndReplicationFailed] -description = "Error when starting the replication and listening of errors for database broadcasting." - -[ReplicationMaxWalSendersReached] -description = "Maximum number of WAL senders reached in tenant database." -[[ReplicationMaxWalSendersReached.references]] -href = "https://supabase.com/docs/guides/database/custom-postgres-config#cli-configurable-settings" -description = "Configuring max WAL senders" - -[MigrationCheckFailed] -description = "Check to see if we require to run migrations fails." - -[PartitionCreationFailed] -description = "Error when creating partitions for realtime.messages." - -[ErrorStartingPostgresCDCStream] -description = "Error when starting the Postgres CDC stream which is used for Postgres Changes." - -[UnknownDataProcessed] -description = "An unknown data type was processed by the Realtime system." - -[ErrorStartingPostgresCDC] -description = "Error when starting the Postgres CDC extension which is used for Postgres Changes." - -[ReplicationSlotBeingUsed] -description = "The replication slot is being used by another transaction." - -[PoolingReplicationPreparationError] -description = "Error when preparing the replication slot." - -[PoolingReplicationError] -description = "Error when pooling the replication slot." - -[SubscriptionDeletionFailed] -description = "Error when trying to delete a subscription for postgres changes." - -[UnableToDeletePhantomSubscriptions] -description = "Error when trying to delete subscriptions that are no longer being used." - -[UnableToCheckProcessesOnRemoteNode] -description = "Error when trying to check the processes on a remote node." - -[UnableToCreateCounter] -description = "Error when trying to create a counter to track rate limits for a tenant." - -[UnableToIncrementCounter] -description = "Error when trying to increment a counter to track rate limits for a tenant." - -[UnableToDecrementCounter] -description = "Error when trying to decrement a counter to track rate limits for a tenant." - -[UnableToUpdateCounter] -description = "Error when trying to update a counter to track rate limits for a tenant." - -[UnableToFindCounter] -description = "Error when trying to find a counter to track rate limits for a tenant." - -[UnhandledProcessMessage] -description = "Unhandled message received by a Realtime process." - -[UnableToTrackPresence] -description = "Error when handling track presence for this socket." - -[UnknownPresenceEvent] -description = "Presence event type not recognized by service." - -[IncreaseConnectionPool] -description = "The number of connections you have set for Realtime are not enough to handle your current use case." - -[RlsPolicyError] -description = "Error on RLS policy used for authorization." - -[ConnectionInitializing] -description = "Database is initializing connection." - -[DatabaseConnectionIssue] -description = "Database had connection issues and connection was not able to be established." - -[UnableToConnectToProject] -description = "Unable to connect to Project database." - -[InvalidJWTExpiration] -description = "JWT exp claim value it's incorrect." - -[JwtSignatureError] -description = "JWT signature was not able to be validated." - -[MalformedJWT] -description = "Token received does not comply with the JWT format." - -[Unauthorized] -description = "Unauthorized access to Realtime channel." - -[RealtimeRestarting] -description = "Realtime is currently restarting." - -[UnableToProcessListenPayload] -description = "Payload sent in NOTIFY operation was not JSON parsable." - -[UnableToListenToTenantDatabase] -description = "Unable to LISTEN for notifications against the Tenant Database." - -[UnprocessableEntity] -description = "Received a HTTP request with a body that was not able to be processed by the endpoint." - -[InitializingProjectConnection] -description = "Connection against Tenant database is still starting." - -[TimeoutOnRpcCall] -description = "RPC request within the Realtime server has timed out." - -[ErrorOnRpcCall] -description = "Error when calling another realtime node." - -[ErrorExecutingTransaction] -description = "Error executing a database transaction in tenant database." - -[SynInitializationError] -description = "Our framework to syncronize processes has failed to properly startup a connection to the database." - -[JanitorFailedToDeleteOldMessages] -description = "Scheduled task for realtime.message cleanup was unable to run." - -[UnableToEncodeJson] -description = "An error were we are not handling correctly the response to be sent to the end user." - -[UnknownErrorOnController] -description = "An error we are not handling correctly was triggered on a controller." - -[UnknownErrorOnChannel] -description = "An error we are not handling correctly was triggered on a channel." 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 bec493599d4..6dbc5d37bfb 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). @@ -104,11 +112,11 @@ The [configuration panel above](#step-2-configure-your-ai-tool) can set these op | `project_ref=` | Scope to a specific project (disables account tools) | `?project_ref=abc123` | | `features=` | Enable only specific tool groups (comma-separated) | `?features=database,docs` | -Parameters can be combined: `https://mcp.supabase.com/mcp?project_ref=abc123&read_only=true` +Parameters can be combined: remote?project_ref=abc123&read_only=true -When using [Supabase CLI](/docs/guides/cli) for local development, the MCP server is available at `http://localhost:54321/mcp`. +When using [Supabase CLI](/docs/guides/cli) for local development, the MCP server is available at local. @@ -131,19 +139,7 @@ To authenticate the MCP server in a CI environment, you can create a personal ac 1. Pass the token to the `Authorization` header in your MCP server configuration. For example if you are using [Claude Code](https://docs.claude.com/en/docs/claude-code/github-actions), your MCP server configuration might look like this: - ```json - { - "mcpServers": { - "supabase": { - "type": "http", - "url": "https://mcp.supabase.com/mcp?project_ref=${SUPABASE_PROJECT_REF}", - "headers": { - "Authorization": "Bearer ${SUPABASE_ACCESS_TOKEN}" - } - } - } - } - ``` + The above example assumes you have environment variables `SUPABASE_ACCESS_TOKEN` and `SUPABASE_PROJECT_REF` set in your CI environment. diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index 4fb8c15b66c..5226d8849aa 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -109,8 +109,8 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
- OpenAI is an AI research and deployment company. Supabase provides a simple way to use - OpenAI in your applications. + OpenAI is an AI research and deployment company. Supabase provides a way to use OpenAI in + your applications.
@@ -125,8 +125,8 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
- Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way - to use Hugging Face's models in your applications. + Hugging Face is an open-source provider of NLP technologies. Supabase provides a way to use + Hugging Face's models in your applications.
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: +The chart below compares HNSW queries-per-second across compute sizes for different embedding dimensions. + multi database +The chart below plots requests-per-second against compute size. + multi database +The chart below shows how the HNSW build parameters `m` and `ef_construction` affect requests-per-second on the dbpedia dataset. + multi database +The chart below shows how the number of IVFFlat lists affects performance for one million vectors. + multi database +_The diagram above shows the vecs benchmark setup: a Python test runner uploads data, builds the index, and runs queries against pgvector._ + Each test is run for a minimum of 30-40 minutes. They include a series of experiments executed at different concurrency levels to measure the engine's performance under different load types. The results are then averaged. As a general recommendation, we suggest using a concurrency level of 5 or more for most workloads and 30 or more for high-load workloads. diff --git a/apps/docs/content/guides/ai/concepts.mdx b/apps/docs/content/guides/ai/concepts.mdx index 984bae34831..73f52fc93d6 100644 --- a/apps/docs/content/guides/ai/concepts.mdx +++ b/apps/docs/content/guides/ai/concepts.mdx @@ -16,7 +16,7 @@ Embeddings capture the "relatedness" of text, images, video, or other types of i - **Classifications:** how do we categorize a body of text? - **Clustering:** how do we identify trends? -Let's explore an example of text embeddings. Say we have three phrases: +The following example uses text embeddings. Given three phrases: 1. "The cat chases the mouse" 2. "The kitten hunts rodents" @@ -39,7 +39,14 @@ So how does this relate to embeddings? Embeddings compress discrete information (words & symbols) into distributed continuous-valued data (vectors). If we took our phrases from before and plot them on a chart, it might look something like this: -Vector similarity +The chart below plots example phrases as points. Phrases with similar meanings sit close together, and unrelated phrases sit far apart. + +A two-dimensional chart plotting example phrases as points, where phrases with similar meanings sit close together and unrelated phrases sit far apart. Phrases 1 and 2 would be plotted close to each other, since their meanings are similar. We would expect phrase 3 to live somewhere far away since it isn't related. If we had a fourth phrase, “Sally ate Swiss cheese”, this might exist somewhere between phrase 3 (cheese can go on sandwiches) and phrase 1 (mice like Swiss cheese). diff --git a/apps/docs/content/guides/ai/engineering-for-scale.mdx b/apps/docs/content/guides/ai/engineering-for-scale.mdx index 55e8990507d..225e4c6c835 100644 --- a/apps/docs/content/guides/ai/engineering-for-scale.mdx +++ b/apps/docs/content/guides/ai/engineering-for-scale.mdx @@ -8,14 +8,16 @@ sidebar_label: 'Engineering for Scale' Content sources for vectors can be extremely large. As you grow you should run your Vector workloads across several secondary databases (sometimes called "pods"), which allows each collection to scale independently. -## Simple workloads +## Small workloads [#simple-workloads] -For small workloads, it's typical to store your data in a single database. +For small workloads, you can typically store your data in a single database. If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different collections, you can expose collections to your web or mobile application using [views](/docs/guides/database/tables#views): +The diagram below shows a single database holding the three vector collections of `docs`, `posts`, and `images`. Each are exposed to your application through a view. + single database diff --git a/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx b/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx index 0110be71963..a2f743a732d 100644 --- a/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx +++ b/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx @@ -231,4 +231,4 @@ Go ahead and test it out by running `poetry run search` and you will be presente ## Conclusion -With just a couple of lines of Python you are able to implement image search as well as reverse image search using the Amazon Titan multimodal model and Supabase Vector. +With a couple of lines of Python you are able to implement image search as well as reverse image search using the Amazon Titan multimodal model and Supabase Vector. diff --git a/apps/docs/content/guides/ai/going-to-prod.mdx b/apps/docs/content/guides/ai/going-to-prod.mdx index ff7ceb25647..e152ee3232a 100644 --- a/apps/docs/content/guides/ai/going-to-prod.mdx +++ b/apps/docs/content/guides/ai/going-to-prod.mdx @@ -6,7 +6,7 @@ subtitle: 'Going to production checklist for AI applications.' sidebar_label: 'Going to Production' --- -This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case. +This guide helps you prepare your application for production. It provides actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case. See our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide for more information about engineering at scale. @@ -72,10 +72,12 @@ The values of lists and probes directly affect accuracy and queries per second ( - Higher `probes` means that select queries will be slower, but you can achieve better accuracy. - `lists` and `probes` are not independent. Higher `lists` means that you will have to use higher `probes` to achieve the same accuracy. -You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blogpost. +You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blog post. + +The chart below shows how the IVFFlat lists count affects accuracy and queries-per-second. multi database ``` -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 @@ -149,7 +149,7 @@ curl --output result.jpg --location --request POST 'http://localhost:54321/funct In this example, your generated image will save to `result.jpg`: -Llama wearing sunglasses example { } ``` -### 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. @@ -220,7 +220,7 @@ export const run = async () => { LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid). -You can install the LangChain Hybrid Search function though our [database.dev package manager](https://database.dev/langchain/hybrid_search). +You can install the LangChain Hybrid Search function through our [database.dev package manager](https://database.dev/langchain/hybrid_search). ## Resources diff --git a/apps/docs/content/guides/ai/python-clients.mdx b/apps/docs/content/guides/ai/python-clients.mdx index a617f0e11cb..a03e5849960 100644 --- a/apps/docs/content/guides/ai/python-clients.mdx +++ b/apps/docs/content/guides/ai/python-clients.mdx @@ -7,7 +7,7 @@ sidebar_label: 'Choosing a Client' As described in [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured), AI workloads come in many forms. -For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started quickly. All you need is a connection string and vecs handles setting up your database to store and query vectors with associated metadata. +For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started. You need a connection string and vecs handles setting up your database to store and query vectors with associated metadata. 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..9ecd87c69ae 100644 --- a/apps/docs/content/guides/ai/rag-with-permissions.mdx +++ b/apps/docs/content/guides/ai/rag-with-permissions.mdx @@ -1,12 +1,12 @@ --- id: 'ai-rag-with-permissions' title: 'RAG with Permissions' -subtitle: 'Fine-grain access control with Retrieval Augmented Generation.' -description: 'Implement fine-grain access control with retrieval augmented generation' +subtitle: 'Fine-grained access control with Retrieval Augmented Generation.' +description: 'Implement fine-grained access control with retrieval augmented generation' sidebar_label: 'RAG with Permissions' --- -Since pgvector is built on top of Postgres, you can implement fine-grain access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase. +Since pgvector is built on top of Postgres, you can implement fine-grained access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase. Use this guide to learn how to restrict access to documents when performing retrieval augmented generation (RAG). @@ -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/semantic-search.mdx b/apps/docs/content/guides/ai/semantic-search.mdx index 76f77ac3582..6146b6a5edd 100644 --- a/apps/docs/content/guides/ai/semantic-search.mdx +++ b/apps/docs/content/guides/ai/semantic-search.mdx @@ -26,7 +26,7 @@ Embeddings are generated using a language model, and embeddings are compared to ## Embedding models -There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings). +There are many embedding models available today. Supabase Edge Functions has [built-in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings). It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results. @@ -65,7 +65,7 @@ For more details on vector columns, including how to generate embeddings and sto ### Similarity metric -`pgvector` support 3 operators for computing distance between embeddings: +`pgvector` supports 3 operators for computing distance between embeddings: | **Operator** | **Description** | | ------------ | ---------------------- | @@ -143,7 +143,7 @@ You can also call this method directly from SQL: select * from match_documents( '[...]'::extensions.vector(512), -- pass the query embedding - 0.78, -- chose an appropriate threshold for your data + 0.78, -- choose an appropriate threshold for your data 10 -- choose the number of matches ); ``` 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/ai/vector-columns.mdx b/apps/docs/content/guides/ai/vector-columns.mdx index 6ca1aa54955..d3ab41c3aa9 100644 --- a/apps/docs/content/guides/ai/vector-columns.mdx +++ b/apps/docs/content/guides/ai/vector-columns.mdx @@ -5,7 +5,7 @@ description: 'Learn how to use vectors within your own Postgres tables' sidebar_label: 'Vector columns' --- -Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section. +Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user, see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section. Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvector/), a Postgres extension for storing and querying vectors in Postgres. It can be used to store [embeddings](/docs/guides/ai/concepts#what-are-embeddings). @@ -48,7 +48,7 @@ To disable an extension, call `drop extension`. ### Create a table to store vectors -After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parenthesis) represents the number of dimensions stored in that vector. +After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parentheses) represents the number of dimensions stored in that vector. ```sql create table documents ( @@ -59,7 +59,7 @@ create table documents ( ); ``` -In the above SQL snippet, we create a `documents` table with a column called `embedding` (note this is just a regular Postgres column - you can name it whatever you like). We give the `embedding` column a `vector` data type with 384 dimensions. Change this to the number of dimensions produced by your embedding model. For example, if you are [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, you would set this number to 384 since that model produces 384 dimensions. +In the SQL snippet above, we create a `documents` table with an `embedding` column. This is a standard Postgres column, so you can name it anything you like. The `embedding` column uses the `vector` data type with 384 dimensions. Change this number to match the dimensions your embedding model produces. For example, if you're [generating embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) using the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model, set this to 384. @@ -73,6 +73,7 @@ In this example we'll generate a vector using Transformers.js, then store it in ```js import { pipeline } from '@huggingface/transformers' + const generateEmbedding = await pipeline('feature-extraction', 'Supabase/gte-small') const title = 'First post!' @@ -99,7 +100,7 @@ This example uses the JavaScript Supabase client, but you can modify it to work ### Querying a vector / embedding -Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for computing distance: +Similarity search is the most common use case for vectors. `pgvector` supports 3 new operators for computing distance: | Operator | Description | | -------- | ---------------------- | diff --git a/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx b/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx index 6390b7260bb..8ddcb7c0ab0 100644 --- a/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx +++ b/apps/docs/content/guides/ai/vector-indexes/hnsw-indexes.mdx @@ -75,8 +75,10 @@ The hierarchical aspect of HNSW builds off of the idea of skip lists. Skip lists are multi-layer linked lists. The bottom layer is a regular linked list connecting an ordered sequence of elements. Each new layer above removes some elements from the underlying layer (based on a fixed probability), producing a sparser subsequence that “skips” over elements. +The diagram below shows a multi-layer skip list. The bottom layer links every ordered element, and each layer above keeps a sparser subset that skips over elements. + visual of an example skip listLog the full `error` object, not just `error.message`. +Log the full `error` object, not only `error.message`. ## The recommended pattern diff --git a/apps/docs/content/guides/api/securing-your-api.mdx b/apps/docs/content/guides/api/securing-your-api.mdx index de875e9e599..894b2e8179b 100644 --- a/apps/docs/content/guides/api/securing-your-api.mdx +++ b/apps/docs/content/guides/api/securing-your-api.mdx @@ -104,7 +104,7 @@ Any table created through the Supabase Dashboard will have RLS enabled by defaul > -1. Go to the [Authentication > Policies](/dashboard/project/_/auth/policies) page in the Dashboard. +1. Go to the [Database > Policies](/dashboard/project/_/database/policies) page in the Dashboard. 2. Select **Enable RLS** to enable Row Level Security. diff --git a/apps/docs/content/guides/auth.mdx b/apps/docs/content/guides/auth.mdx index a500f15609c..fd4071f7e65 100644 --- a/apps/docs/content/guides/auth.mdx +++ b/apps/docs/content/guides/auth.mdx @@ -1,7 +1,7 @@ --- id: 'auth' title: 'Auth' -description: 'Use Supabase to Authenticate and Authorize your users.' +description: 'Use Supabase to authenticate and authorize your users.' subtitle: 'Use Supabase to authenticate and authorize your users.' tocVideo: '6ow_jW4epf8' --- @@ -27,19 +27,15 @@ Auth uses your project's Postgres database under the hood, storing user data and Auth also enables access control to your database's automatically generated [REST API](/docs/guides/api). When using Supabase SDKs, your data requests are automatically sent with the user's Auth Token. The Auth Token scopes database access on a row-by-row level when used along with [RLS policies](/docs/guides/database/postgres/row-level-security). + + <$Show if="authentication:show_providers"> <$Partial path="providers.mdx" /> <$Show if="billing:all"> -## Pricing - -Charges apply to Monthly Active Users (MAU), Monthly Active Third-Party Users (Third-Party MAU), and Monthly Active SSO Users (SSO MAU) and Advanced MFA Add-ons. For a detailed breakdown of how these charges are calculated, refer to the following pages: - -- [Pricing MAU](/docs/guides/platform/manage-your-usage/monthly-active-users) -- [Pricing Third-Party MAU](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party) -- [Pricing SSO MAU](/docs/guides/platform/manage-your-usage/monthly-active-users-sso) -- [Advanced MFA - Phone](/docs/guides/platform/manage-your-usage/advanced-mfa-phone) - + + + diff --git a/apps/docs/content/guides/auth/auth-anonymous.mdx b/apps/docs/content/guides/auth/auth-anonymous.mdx index c8d7f823f92..0a284cc4c8e 100644 --- a/apps/docs/content/guides/auth/auth-anonymous.mdx +++ b/apps/docs/content/guides/auth/auth-anonymous.mdx @@ -8,7 +8,7 @@ subtitle: 'Create and use anonymous users to authenticate with Supabase' -Calling `signInAnonymously()` creates an anonymous user. It's just like a permanent user, except the user can't access their account if they sign out, clear browsing data, or use another device. +Calling `signInAnonymously()` creates an anonymous user. It behaves like a permanent user, except the user can't access their account if they sign out, clear browsing data, or use another device. Like permanent users, the `authenticated` Postgres role will be used when using the Data APIs to access your project. JWTs for these users will have an `is_anonymous` claim which you can use to distinguish in RLS policies. @@ -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. @@ -279,7 +279,7 @@ response = supabase.auth.link_identity({'provider': 'google'}) ## Access control -An anonymous user assumes the `authenticated` role just like a permanent user. You can use row-level security (RLS) policies to differentiate between an anonymous user and a permanent user by checking for the `is_anonymous` claim in the JWT returned by `auth.jwt()`: +An anonymous user assumes the `authenticated` role like a permanent user. You can use row-level security (RLS) policies to differentiate between an anonymous user and a permanent user by checking for the `is_anonymous` claim in the JWT returned by `auth.jwt()`: ```sql create policy "Only permanent users can post to the news feed" 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 auth.rate_limits.magic_link.period and they expire after auth.rate_limits.magic_link.validity. @@ -87,7 +87,7 @@ Read the [Deep Linking Documentation](/docs/guides/auth/native-mobile-deep-linki ```dart Future signInWithEmail() async { - final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io'); + await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io'); } ``` @@ -173,7 +173,7 @@ Email one-time passwords (OTP) are a form of passwordless login where users key Email authentication methods, including Email OTPs, are enabled by default. -Email OTPs share an implementation with Magic Links. To send an OTP instead of a Magic Link, alter the **Magic Link** email template. For a hosted Supabase project, go to [Email Templates](/dashboard/project/_/auth/templates) in the Dashboard. For a self-hosted project or local development, see the [Email Templates guide](/docs/guides/auth/auth-email-templates). +Email OTPs share an implementation with Magic Links. To send an OTP instead of a Magic Link, alter the **Magic Link** [email template](/dashboard/project/_/auth/templates/magic-link-or-otp). Refer to the [Email Templates guide](/docs/guides/auth/auth-email-templates) for more information. Modify the template to include the `{{ .Token }}` variable, for example: @@ -183,7 +183,13 @@ Modify the template to include the `{{ .Token }}` variable, for example:

Please enter this code: {{ .Token }}

``` -By default, a user can only request an OTP once every auth.rate_limits.otp.period and they expire after auth.rate_limits.otp.validity. This is configurable via `Auth > Providers > Email > Email OTP Expiration`. An expiry duration of more than 86400 seconds (one day) is disallowed to guard against brute force attacks. The longer an OTP remains valid, the more time an attacker has to attempt brute force attacks. If the OTP is valid for several days, an attacker might have more opportunities to guess the correct OTP through repeated attempts. +By default, a user can only request an OTP once every auth.rate_limits.otp.period, and they expire after auth.rate_limits.otp.validity. This is configurable via **Authentication > Sign In / Providers > Auth Providers > Email > Email OTP expiration**. An expiry duration of more than 86,400 seconds (one day) is strongly discouraged and can only be set via the [Management API](/docs/reference/api/v1-update-auth-service-config). Make sure to read the [security recommendations](/docs/guides/deployment/going-into-prod#security) before going into production. + + + +The **Email OTP Expiration** setting also governs the validity of Magic Links and other email links, including confirmation, password recovery, email change, and [invitation](/docs/guides/auth/users#inviting-users) links. + + ### Signing in with email OTP @@ -223,7 +229,7 @@ const { data, error } = await supabase.auth.signInWithOtp({ ```dart Future signInWithEmailOtp() async { - final AuthResponse res = await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io'); + await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io'); } ``` diff --git a/apps/docs/content/guides/auth/auth-email-templates.mdx b/apps/docs/content/guides/auth/auth-email-templates.mdx index 34fe78b1811..5be7d6d7384 100644 --- a/apps/docs/content/guides/auth/auth-email-templates.mdx +++ b/apps/docs/content/guides/auth/auth-email-templates.mdx @@ -49,7 +49,27 @@ The templating system provides the following variables for use: ## Editing email templates -On hosted Supabase projects, edit your email templates on the [Email Templates](/dashboard/project/_/auth/templates) page. On self-hosted projects or in local development, edit your [configuration files](/docs/guides/local-development/customizing-email-templates). +Where you edit templates depends on how you run Supabase. + +### Hosted projects + +Edit templates on the [Email Templates](/dashboard/project/_/auth/templates) page in the dashboard. The template builder uses the same [terminology](#terminology) and variables documented on this page. + +### Local development and self-hosted + +The dashboard template builder **does not apply** when running [local development with CLI](/docs/guides/local-development) or [self-hosted Supabase](/docs/guides/self-hosting). Customize templates in `supabase/config.toml` and local HTML files instead. + + + +See [Customizing email templates](/docs/guides/local-development/customizing-email-templates) for: + +- [`config.toml` keys and HTML file paths](/docs/guides/local-development/customizing-email-templates#configuring-templates) +- [Template variables](/docs/guides/local-development/customizing-email-templates#template-variables) — the same placeholders as the [terminology](#terminology) table above +- Default subjects and behavior for each [authentication](/docs/guides/local-development/customizing-email-templates#available-authentication-email-templates) and [security notification](/docs/guides/local-development/customizing-email-templates#available-security-notification-email-templates) template + +For self-hosted deployments, refer to [Custom email templates](/docs/guides/self-hosting/custom-email-templates). + + You can also manage email templates using the Management API: diff --git a/apps/docs/content/guides/auth/auth-hooks/before-user-created-hook.mdx b/apps/docs/content/guides/auth/auth-hooks/before-user-created-hook.mdx index df2b1199cda..c76f9deadf8 100644 --- a/apps/docs/content/guides/auth/auth-hooks/before-user-created-hook.mdx +++ b/apps/docs/content/guides/auth/auth-hooks/before-user-created-hook.mdx @@ -19,7 +19,7 @@ Supabase Auth will send a payload containing these fields to your hook: -Because the hook is ran just before the insertion into the database, this user will not be found in Postgres at the time the hook is called. +Because the hook runs immediately before insertion into the database, this user will not be found in Postgres at the time the hook is called. @@ -722,8 +722,8 @@ supabase functions new before-user-created-hook Add the following code to your edge function: ```ts -import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' import { createClient } from 'https://esm.sh/@supabase/supabase-js' +import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' const whSecret = Deno.env.get('BEFORE_USER_CREATED_HOOK_SECRET')?.replace('v1,whsec_', '') const supabaseUrl = Deno.env.get('SUPABASE_URL') diff --git a/apps/docs/content/guides/auth/auth-mfa.mdx b/apps/docs/content/guides/auth/auth-mfa.mdx index 1cb3576ef18..e7e1744b5c1 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. */ @@ -271,7 +271,7 @@ It is possible to enforce MFA on the Server-Side Rendering level. However, this You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and `supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session and any factors that are enabled for a user, similar to how you would use these on the browser. -However, encountering a different AAL level on the server may not actually be a security problem. Consider these likely scenarios: +However, encountering a different AAL level on the server may not be a security problem. Consider these likely scenarios: 1. User signed-in with a conventional method but closed their tab on the MFA flow. @@ -284,7 +284,7 @@ We thus recommend you redirect users to a page where they can authenticate using ### APIs -If your application uses the Supabase Database, Storage or Edge Functions, just using Row Level Security policies will give you sufficient protection. In the event that you have other APIs that you wish to protect, follow these general guidelines: +If your application uses the Supabase Database, Storage or Edge Functions, Row Level Security policies provide sufficient protection. In the event that you have other APIs that you wish to protect, follow these general guidelines: 1. **Use a good JWT verification and parsing library for your language.** This will let you securely parse JWTs and extract their claims. @@ -300,7 +300,6 @@ If your application uses the Supabase Database, Storage or Edge Functions, just How do I check when a user went through MFA?} +header="How do I check when a user went through MFA?" id="how-do-i-check-when-a-user-went-through-mfa" > diff --git a/apps/docs/content/guides/auth/auth-mfa/phone.mdx b/apps/docs/content/guides/auth/auth-mfa/phone.mdx index c903dd95165..2bf410157f7 100644 --- a/apps/docs/content/guides/auth/auth-mfa/phone.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/phone.mdx @@ -12,23 +12,36 @@ The phone messaging configuration for MFA is shared with [phone auth login](/doc Below is a flow chart illustrating how the Enrollment and Verify APIs work in the context of MFA (Phone). -Diagram showing the flow of Multi-Factor authentication +```mermaid +flowchart TD + InitS((Setup flow)) --> SAAL1[/Session is AAL1/] + SAAL1 --> Enroll[Enroll API] + Enroll --> ChallengeAPI[Challenge API] + ChallengeAPI --> Scan[/Code sent to User/] + Scan --> Enter[User: Enter code] + Enter --> Verify[Verify API] + Verify --> Check{{Is code correct?}} + Check -->|Yes| AAL2[/Upgrade to AAL2/] + AAL2 --> Done((Done)) + Check -->|No| Enter + InitA((Login flow)) --> SignIn([User: Sign-in]) + SignIn --> AAL1[/Upgrade to AAL1/] + AAL1 --> ListFactors[List Factors API] + ListFactors -->|1 or more factors| OpenAuth([User: Select phone factor]) + OpenAuth --> Enter + ListFactors -->|0 factors| Setup[[Setup flow]] +``` + +In the **setup flow**, a session already at AAL1 calls the Enroll API followed by the Challenge API, which sends a code to the user over SMS or WhatsApp. The user enters the code, the Verify API checks it, and on success the session is upgraded to AAL2. An incorrect code returns the user to the code-entry step. + +In the **login flow**, the user signs in (upgrading the session to AAL1) and the List Factors API is called. If the user has one or more factors, they select their phone factor and enter the code that was sent, following the same Verify path to reach AAL2. If they have no factors enrolled, they are sent through the setup flow first. ### Add enrollment flow An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app: 1. Right after login or sign up. - This allows users quickly set up Multi Factor Authentication (MFA) post login or account creation. Where possible, encourage all users to set up MFA. Many applications offer this as an opt-in step in an + This allows users to set up Multi Factor Authentication (MFA) post login or account creation. Where possible, encourage all users to set up MFA. Many applications offer this as an opt-in step in an effort to reduce onboarding friction. 2. From within a settings page. Allows users to set up, disable or modify their MFA settings. diff --git a/apps/docs/content/guides/auth/auth-mfa/totp.mdx b/apps/docs/content/guides/auth/auth-mfa/totp.mdx index c645e15f681..c45751d91e9 100644 --- a/apps/docs/content/guides/auth/auth-mfa/totp.mdx +++ b/apps/docs/content/guides/auth/auth-mfa/totp.mdx @@ -12,25 +12,42 @@ The use of a QR code was [initially introduced by Google Authenticator](https:// Below is a flow chart illustrating how the Enrollment, Challenge, and Verify APIs work in the context of MFA (TOTP). -Diagram showing the flow of Multi-Factor authentication +```mermaid +flowchart TD + InitS((Setup flow)) --> SAAL1[/Session is AAL1/] + SAAL1 --> Enroll[Enroll API] + Enroll --> ShowQR[Show QR code] + ShowQR --> Scan([User: Scan QR code in authenticator]) + Scan --> Enter([User: Enter code]) + Enter --> Verify[Challenge + Verify API] + Verify --> Check{{Is code correct?}} + Check -->|Yes| AAL2[/Upgrade to AAL2/] + AAL2 --> Done((Done)) + Check -->|No| Enter + InitA((Login flow)) --> SignIn([User: Sign-in]) + SignIn --> AAL1[/Upgrade to AAL1/] + AAL1 --> ListFactors[List Factors API] + ListFactors -->|1 or more factors| OpenAuth([User: Open authenticator]) + OpenAuth --> Enter + ListFactors -->|0 factors| Setup[[Setup flow]] +``` + +In the **setup flow**, a session already at AAL1 calls the Enroll API, which returns a QR code for the user to scan with their authenticator app. The user enters the generated code, the Challenge and Verify APIs check it, and on success the session is upgraded to AAL2. If the code is incorrect, the user is prompted to enter it again. + +In the **login flow**, the user signs in (upgrading the session to AAL1) and the List Factors API is called. If the user has one or more factors, they open their authenticator and enter a code, which follows the same Challenge and Verify path to reach AAL2. If they have no factors enrolled, they are sent through the setup flow first. + + [TOTP MFA API](/docs/reference/javascript/auth-mfa-api) is free to use and is enabled on all Supabase projects by default. + + ### Add enrollment flow An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app: 1. Right after login or sign up. - This lets users quickly set up MFA immediately after they log in or create an + This lets users set up MFA immediately after they log in or create an account. We recommend encouraging all users to set up MFA if that makes sense for your application. Many applications offer this as an opt-in step in an effort to reduce onboarding friction. @@ -75,7 +92,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. @@ -294,29 +311,6 @@ function AuthMFA() { ## Frequently asked questions - - -What's inside the QR code?} -id="what-is-inside-the-qr-code" -> - - - -How long is the TOTP code valid for?} -id="how-long-is-the-totp-code-valid-for" -> +### How long is the TOTP code valid for? In our TOTP implementation, each generated code remains valid for one interval, which spans 30 seconds. To account for minor time discrepancies, we allow for a one-interval clock skew. This ensures that users can successfully authenticate within this timeframe, even if there are slight variations in system clocks. - - - - diff --git a/apps/docs/content/guides/auth/auth-smtp.mdx b/apps/docs/content/guides/auth/auth-smtp.mdx index 2e4d9fefd33..7ff2e248828 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.** @@ -118,7 +118,7 @@ This includes: **Have another SMTP service set up on stand-by.** -In case the primary SMTP service you're using is experiencing difficulty, or your account is under threat of being blocked due to spam, you have another service to quickly turn to. +In case the primary SMTP service you're using is experiencing difficulty, or your account is under threat of being blocked due to spam, you have another service to turn to. **Use consistent branding and focused content.** 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/general-configuration.mdx b/apps/docs/content/guides/auth/general-configuration.mdx index 0e79c0625c7..fcb98df2791 100644 --- a/apps/docs/content/guides/auth/general-configuration.mdx +++ b/apps/docs/content/guides/auth/general-configuration.mdx @@ -6,7 +6,7 @@ subtitle: 'General configuration options for Supabase Auth' This section covers the [general configuration options](/dashboard/project/_/auth) for Supabase Auth. If you are looking for another type of configuration, you may be interested in one of the following sections: -- [Policies](/dashboard/project/_/auth/policies) to manage Row Level Security policies for your tables. +- [Policies](/dashboard/project/_/database/policies) to manage Row Level Security policies for your tables. - [Sign In / Providers](/dashboard/project/_/auth/providers) to configure authentication providers and login methods for your users. - [Third Party Auth](/dashboard/project/_/auth/third-party) to use third-party authentication (TPA) systems based on JWTs to access your project. - [Sessions](/dashboard/project/_/auth/sessions) to configure settings for user sessions and refresh tokens. diff --git a/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx b/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx index 7b890e169b6..e08ddeb5567 100644 --- a/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx +++ b/apps/docs/content/guides/auth/native-mobile-deep-linking.mdx @@ -120,7 +120,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This - Go to your [auth settings](/dashboard/project/_/auth/url-configuration) page. - You need to enter your app redirect callback on `Additional Redirect URLs` field. - The redirect callback URL should have this format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.flutterquickstart://login-callback` is just an example, you can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used. + The redirect callback URL should have this format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.flutterquickstart://login-callback` is an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used. ![Supabase console deep link setting](/docs/img/deeplink-setting.png) @@ -314,7 +314,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This 1. Go to your [auth settings](/dashboard/project/_/auth/url-configuration) page. 2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link. - The redirect callback URL should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used. + The redirect callback URL should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used. ![Supabase console deep link setting](/docs/img/deeplink-setting.png) @@ -356,7 +356,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This 1. Go to your [auth settings](/dashboard/project/_/auth/url-configuration) page. 2. Enter your app redirect URL in the `Additional Redirect URLs` field. This is the URL that the user gets redirected to after clicking a magic link. - The redirect callback URL should have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. Here, `io.supabase.user-management://login-callback` is just an example. You can choose whatever you would like for `YOUR_SCHEME` and `YOUR_HOSTNAME` as long as the scheme is unique across the user's device. For this reason, typically a reverse domain of your website is used. + The redirect callback URL must have the format `[YOUR_SCHEME]://[YOUR_HOSTNAME]`. For example: `io.supabase.user-management://login-callback`. You can use any values for `YOUR_SCHEME` and `YOUR_HOSTNAME`, but the scheme must be unique across the user's device. For this reason, a reverse domain of your website is typically used. Now, edit the Android manifest to make sure the app opens when the user clicks on the magic link. diff --git a/apps/docs/content/guides/auth/oauth-server.mdx b/apps/docs/content/guides/auth/oauth-server.mdx index 31df7fae5fc..66c7790d320 100644 --- a/apps/docs/content/guides/auth/oauth-server.mdx +++ b/apps/docs/content/guides/auth/oauth-server.mdx @@ -3,7 +3,7 @@ title: 'OAuth 2.1 Server' description: 'Turn your Supabase project into an OAuth 2.1 and OpenID Connect identity provider' --- -Supabase Auth can act as an OAuth 2.1 and OpenID Connect (OIDC) identity provider. This allows other applications and services to use your Supabase project as their authentication provider, just like "Sign in with Google" or "Sign in with GitHub". +Supabase Auth can act as an OAuth 2.1 and OpenID Connect (OIDC) identity provider. This allows other applications and services to use your Supabase project as their authentication provider, like "Sign in with Google" or "Sign in with GitHub". You can use this to build "Sign in with [Your App]" experiences, authenticate AI agents through the Model Context Protocol (MCP), power developer platforms with third-party integrations, or implement standards-compliant enterprise SSO. 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/oauth-server/mcp-authentication.mdx b/apps/docs/content/guides/auth/oauth-server/mcp-authentication.mdx index 051892f1ad5..7434553409f 100644 --- a/apps/docs/content/guides/auth/oauth-server/mcp-authentication.mdx +++ b/apps/docs/content/guides/auth/oauth-server/mcp-authentication.mdx @@ -79,13 +79,13 @@ When building your own MCP server, integrate with Supabase Auth to authenticate **Looking for an easier way to build MCP servers?** -[FastMCP](https://gofastmcp.com) provides a streamlined way to build MCP servers with built-in Supabase Auth integration. FastMCP handles OAuth configuration, token management, and authentication flows automatically, letting you focus on building your AI agent's functionality. Check out their [Supabase integration guide](https://gofastmcp.com/integrations/supabase#supabase-fastmcp) to get started quickly. +[FastMCP](https://gofastmcp.com) provides a streamlined way to build MCP servers with built-in Supabase Auth integration. FastMCP handles OAuth configuration, token management, and authentication flows automatically, letting you focus on building your AI agent's functionality. Check out their [Supabase integration guide](https://gofastmcp.com/integrations/supabase#supabase-fastmcp) to get started.
## Handling MCP tokens in your application -When your MCP server makes requests to your Supabase APIs on behalf of authenticated users, it will send access tokens issued by Supabase Auth, just like any other OAuth client. +When your MCP server makes requests to your Supabase APIs on behalf of authenticated users, it will send access tokens issued by Supabase Auth, like any other OAuth client. ### Validating MCP tokens diff --git a/apps/docs/content/guides/auth/oauth-server/token-security.mdx b/apps/docs/content/guides/auth/oauth-server/token-security.mdx index b5da836eb94..2fe88b440a2 100644 --- a/apps/docs/content/guides/auth/oauth-server/token-security.mdx +++ b/apps/docs/content/guides/auth/oauth-server/token-security.mdx @@ -368,7 +368,7 @@ SELECT * FROM user_data WHERE user_id = 'test-user-uuid'; RESET request.jwt.claims; ``` -Or use the Supabase Dashboard's [RLS policy tester](/dashboard/project/_/auth/policies). +Or use the Supabase Dashboard's [RLS policy tester](/dashboard/project/_/database/policies). ## Troubleshooting diff --git a/apps/docs/content/guides/auth/passkeys.mdx b/apps/docs/content/guides/auth/passkeys.mdx index d97e1ed3f31..ce5df61be30 100644 --- a/apps/docs/content/guides/auth/passkeys.mdx +++ b/apps/docs/content/guides/auth/passkeys.mdx @@ -14,7 +14,7 @@ Passkey support is experimental. The API may change without notice. You must exp -**Requires `@supabase/supabase-js` v2.105.0 and later.** Upgrade your client library to use passkey authentication. +**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. @@ -23,7 +23,7 @@ Passkey support is experimental. The API may change without notice. You must exp 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 browser invokes `navigator.credentials.create()` (registration) or `navigator.credentials.get()` (authentication), prompting the user for biometrics or a security key. +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. @@ -39,7 +39,10 @@ Open the [Passkeys settings](/dashboard/project/_/auth/passkeys) from the **Auth - **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"). 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. Up to 5 origins. +- **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. @@ -99,6 +102,15 @@ Passkey support is currently experimental and requires explicit opt-in as the AP + + + ```ts import { createClient } from '@supabase/supabase-js' @@ -109,11 +121,61 @@ const supabase = createClient(supabaseUrl, supabaseKey, { }) ``` + + + +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; +``` + +`supabase_flutter` performs the server side of the WebAuthn ceremony for you and delegates the platform prompt (FaceID/TouchID/security key) to an authenticator you supply, instead of depending on a passkey plugin directly. Add a passkey plugin to your own app and pass its authenticator to `registerPasskey()` and `signInWithPasskey()`. The [`passkeys`](https://pub.dev/packages/passkeys) plugin's `PasskeyAuthenticator` implements the `PasskeyAuthenticatorInterface` these methods expect (since `passkeys` `2.21.0`), but you can pass any implementation of that interface: + +```dart +import 'package:passkeys/authenticator.dart'; + +final authenticator = PasskeyAuthenticator(); +``` + +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 browser API, and verifies the response with Supabase Auth. +`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() @@ -126,11 +188,49 @@ if (error) { } ``` -The returned `data` contains the new passkey's metadata: + + + +```dart +try { + final Passkey passkey = await supabase.auth.registerPasskey(authenticator); + 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 + id: string // UUID — use this to update or delete the passkey friendly_name?: string // Derived from the authenticator's AAGUID created_at: string } @@ -138,12 +238,21 @@ The returned `data` contains the new passkey's metadata: 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](/docs/reference/javascript/auth-registerpasskey) for the full API. +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() @@ -155,12 +264,56 @@ if (error) { } ``` -See the [`signInWithPasskey` reference](/docs/reference/javascript/auth-signinwithpasskey) for the full API. + + + +```dart +try { + final AuthResponse res = await supabase.auth.signInWithPasskey(authenticator); + // 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 @@ -185,14 +338,86 @@ const { data } = await supabase.auth.passkey.verifyAuthentication({ }) ``` -The `options` field returned from `startRegistration` and `startAuthentication` 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](/docs/reference/javascript/auth-passkey-api) for the full API. +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() @@ -208,14 +433,62 @@ await supabase.auth.passkey.update({ await supabase.auth.passkey.delete({ passkeyId: passkeys[0].id }) ``` -`friendlyName` is limited to 120 characters. `last_used_at` is updated each time the passkey is used to sign in. + + -See the [`auth.passkey` reference](/docs/reference/javascript/auth-passkey-api) for the full API. +```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' @@ -228,7 +501,26 @@ const { data } = await supabase.auth.admin.passkey.listPasskeys({ userId }) await supabase.auth.admin.passkey.deletePasskey({ userId, passkeyId }) ``` -See the [`auth.admin.passkey` reference](/docs/reference/javascript/auth-admin-passkey-api) for the full API. + + + +```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 diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index 4ad2eb6aafb..ad37b727915 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -28,9 +28,8 @@ The instructions in this section assume that email confirmations are enabled. stickyTabList={{ style: { top: 'var(--header-height)', - backgroundColor: 'hsl(var(--background-default) / var(--tw-bg-opacity))', maskImage: 'none', - borderBottom: '1px solid hsl(var(--border-default) / 1)', + borderBottom: '1px solid oklch(from var(--border-default) l c h / 1)', } }} size="large" @@ -1036,7 +1035,7 @@ final UserResponse res = await supabase.auth.updateUser( #### Verifying the current password -If your app requires users to confirm their current password before setting a new one, you can pass `currentPassword` (available in `supabase-js` v2.102.0+ and `supabase-kt` 3.5.0+): +If your app requires users to confirm their current password before setting a new one, you can pass `current_password` (available in `supabase-js` v2.102.0+ and `supabase-kt` 3.5.0+): +<$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/astrojs.mdx b/apps/docs/content/guides/auth/quickstarts/astrojs.mdx index e0de5fba90f..1baa7521c43 100644 --- a/apps/docs/content/guides/auth/quickstarts/astrojs.mdx +++ b/apps/docs/content/guides/auth/quickstarts/astrojs.mdx @@ -106,7 +106,7 @@ hideToc: true PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_key ``` - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} /> + <$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} /> diff --git a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx index 82116d56820..30912b80e7e 100644 --- a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx @@ -67,7 +67,7 @@ hideToc: true NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_... key ``` - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} /> + <$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} /> @@ -95,5 +95,5 @@ hideToc: true ## Learn more -- [Setting up Server-Side Auth for Next.js](/docs/guides/auth/server-side/nextjs) for a Next.js deep dive +- [Setting up Server-Side Auth for Next.js](/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=nextjs) for a Next.js deep dive - [Supabase Auth docs](/docs/guides/auth#authentication) for more Supabase authentication methods diff --git a/apps/docs/content/guides/auth/quickstarts/react-native.mdx b/apps/docs/content/guides/auth/quickstarts/react-native.mdx index 4318c0d3e3b..d8ed113a884 100644 --- a/apps/docs/content/guides/auth/quickstarts/react-native.mdx +++ b/apps/docs/content/guides/auth/quickstarts/react-native.mdx @@ -81,7 +81,7 @@ hideToc: true meta="name=lib/supabase.ts" /> - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "exporeactnative", "tab": "mobiles" }} /> + <$Partial path="api_settings.mdx" variables={{ "framework": "exporeactnative", "tab": "mobiles" }} /> diff --git a/apps/docs/content/guides/auth/quickstarts/react.mdx b/apps/docs/content/guides/auth/quickstarts/react.mdx index 7c99938fecd..63cc239e117 100644 --- a/apps/docs/content/guides/auth/quickstarts/react.mdx +++ b/apps/docs/content/guides/auth/quickstarts/react.mdx @@ -65,8 +65,7 @@ hideToc: true Rename `.env.example` to `.env.local` and populate with your Supabase connection variables: - - + <$Partial path="api_settings.mdx" variables={{ "framework": "react", "tab": "frameworks" }} /> @@ -77,7 +76,6 @@ hideToc: true lines={[[1, -1]]} meta="name=.env.local" /> - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "react", "tab": "frameworks" }} /> diff --git a/apps/docs/content/guides/auth/server-side.mdx b/apps/docs/content/guides/auth/server-side.mdx index 782db4bc501..81ae770cd4a 100644 --- a/apps/docs/content/guides/auth/server-side.mdx +++ b/apps/docs/content/guides/auth/server-side.mdx @@ -15,7 +15,7 @@ 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 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..41dfa60552b 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 @@ -41,10 +41,7 @@ pnpm add @supabase/supabase-js @supabase/ssr Create a `.env.local` file in the project root directory. In the file, set the project's Supabase URL and Key: - - - -<$Partial path="api_settings_steps.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} /> +<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} /> @@ -172,6 +169,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" /> + + What does the `cookies` object do?} + header="What does the `cookies` object do?" id="utility-cookies" > @@ -212,7 +213,7 @@ The Proxy is responsible for: Do I need to create a new client for every route?} + header="Do I need to create a new client for every route?" id="client-deduplication" > @@ -258,6 +259,8 @@ It's safe to trust `getClaims()` because it validates the JWT signature against +<$Partial path="auth_methods.mdx" /> +
<$CodeTabs> <$CodeSample path="/auth/nextjs/proxy.ts" meta="name=proxy.ts" language="typescript" /> @@ -291,6 +294,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 +614,7 @@ export default defineEventHandler(async (event) => { } ) - await supabase.auth.getUser() + await supabase.auth.getClaims() return { ok: true } }) @@ -843,6 +848,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/signing-keys.mdx b/apps/docs/content/guides/auth/signing-keys.mdx index 2fcd8b3ce59..97518c9618a 100644 --- a/apps/docs/content/guides/auth/signing-keys.mdx +++ b/apps/docs/content/guides/auth/signing-keys.mdx @@ -74,31 +74,12 @@ Key rotation and revocation are one of the most important processes for maintain ### Lifetime of a signing key -
- -Diagram showing the state transitions of a signing key - -
- A newly created key starts off as standby, before being rotated into in use (becoming the current key) while the existing current key becomes previously used. At any point you can move a key from the previously used or revoked states back to being a standby key, and rotate to it. This gives you the confidence to revert back to an older key if you identify problems with the rotation, such as forgetting to update a component of your application that is relying on a specific key (for example, the legacy JWT secret). Each action on a key is reversible (except permanent deletion). -
- -
- | Action | Accepted JWT signatures | Description | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create a new key | Current key only, new key has not created any JWTs yet. | When you initially create a key, after choosing the signing algorithm or importing a private key you already have, it starts out in the standby state. If using an asymmetric key (RSA, Elliptic Curve) its public key will be available in the discovery endpoint. Supabase Auth does not use this key to create new JWTs. | @@ -170,7 +151,7 @@ This guarantee provides your application with close alignment with security comp If you wish to make your own JWTs or have access to the private key or shared secret used by Supabase, you can create a new JWT signing key by importing a private key or setting a shared secret yourself. -Use the [Supabase CLI](/docs/reference/cli/introduction) to quickly and securely generate a private key ready for import: +Use the [Supabase CLI](/docs/reference/cli/introduction) to securely generate a private key ready for import: ```sh supabase gen signing-key --algorithm ES256 @@ -247,7 +228,7 @@ This is to ensure you have the ability, should you need it, to go back to the le ### Why does revoking the legacy JWT secret require disabling of `anon` and `service_role` API keys? -Unfortunately `anon` and `service_role` are not just API keys, but are also valid JSON Web Tokens, signed by the legacy JWT secret. Revoking the legacy JWT secret means that your application no longer trusts any JWT signed with it. Therefore before you revoke the legacy JWT secret, you must disable the `anon` and `service_role` to ensure a consistent security setup. +Unfortunately `anon` and `service_role` are not only API keys, but are also valid JSON Web Tokens, signed by the legacy JWT secret. Revoking the legacy JWT secret means that your application no longer trusts any JWT signed with it. Therefore before you revoke the legacy JWT secret, you must disable the `anon` and `service_role` to ensure a consistent security setup. ### Using JWT-based `anon` key in a mobile, desktop, or CLI application and need to rotate a `service_role` JWT secret? diff --git a/apps/docs/content/guides/auth/social-login/auth-azure.mdx b/apps/docs/content/guides/auth/social-login/auth-azure.mdx index 0189baaad42..7e2a09b236c 100644 --- a/apps/docs/content/guides/auth/social-login/auth-azure.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-azure.mdx @@ -95,7 +95,7 @@ Configure this in the following way: - Select the _App registrations_ menu in Microsoft Entra ID on the Azure portal. - Select the OAuth app. - Select the _Manifest_ menu in the sidebar. -- Make a backup of the JSON just in case. +- Make a backup of the JSON in case you need it later. - Identify the `optionalClaims` key. - Edit it by specifying the following object: ```json 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/users.mdx b/apps/docs/content/guides/auth/users.mdx index 52306d68b72..0ad2d0c0eff 100644 --- a/apps/docs/content/guides/auth/users.mdx +++ b/apps/docs/content/guides/auth/users.mdx @@ -21,7 +21,7 @@ See the [Anonymous Signins guide](/docs/guides/auth/auth-anonymous) to learn mor -Just like permanent users, anonymous users use the **authenticated** role for database access. +Like permanent users, anonymous users use the **authenticated** role for database access. The **anon** role is for those who aren't signed in at all and are not tied to any user ID. We refer to these as unauthenticated or public users. @@ -74,6 +74,62 @@ The user object contains the following attributes: | updated_at | `string` | The timestamp that the user was last updated. | | is_anonymous | `boolean` | Is true if the user is an anonymous user. | +## Inviting users + +You can invite someone to create an account by sending them an invitation email. The invited user receives an email containing a link that, when clicked, confirms their email address and lets them finish setting up their account (for example, by setting a password). + +Inviting a user is an admin action, so it must be performed from a trusted server environment using your secret key, or from the Dashboard. When you invite an email that doesn't yet belong to a user, a new unconfirmed user is created. Inviting an email that already belongs to a confirmed user returns an error. + +### Using the Dashboard + +1. Go to **Authentication > Users** in the Dashboard. +2. Click **Add user** and select **Send invitation**. +3. Enter the user's email address and click **Invite user**. + +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} + +### Using the Auth Admin API + +Call [`inviteUserByEmail()`](/docs/reference/javascript/auth-admin-inviteuserbyemail) from the SDK's Auth Admin API in a server-side environment. This is part of Supabase Auth (accessed via `supabase.auth.admin` with your project's [secret key](/docs/guides/getting-started/api-keys)), and is distinct from the [Management API](/docs/reference/api/introduction) used to configure your project. You can optionally attach custom `user_metadata` and a redirect URL for the invite link. + +```js +import { createClient } from '@supabase/supabase-js' + +// Use your project's secret key (sb_secret_...), and only ever on a trusted server. +const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SECRET_KEY, { + auth: { + autoRefreshToken: false, + persistSession: false, + detectSessionInUrl: false, + }, +}) + +const { data, error } = await supabase.auth.admin.inviteUserByEmail('someone@example.com', { + data: { name: 'Jane' }, // optional, stored in user_metadata + redirectTo: 'https://example.com/welcome', // optional, where the invite link sends the user +}) +``` + + + +The secret key (`sb_secret_...`, which replaces the legacy `service_role` key) bypasses Row Level Security and must only be used in a secure server environment. Never expose it in a browser or any publicly accessible client. + + + + + +The `redirectTo` URL must be in your project's [allowed redirect URLs](/docs/guides/auth/redirect-urls) configuration. If it isn't, the `redirectTo` value is ignored and the invite link redirects to your Site URL instead (no error is raised). + + + +The invitation email uses the **Invite user** email template, which you can customize. Refer to [Email Templates](/docs/guides/auth/auth-email-templates) to learn more. + + + +Invitation links expire after the duration configured in [Email OTP Expiration](/dashboard/project/_/auth/providers?provider=Email), which defaults to 1 hour. This is the same value used for [email OTPs](/docs/guides/auth/auth-email-passwordless#enabling-email-otp), magic links, and other email confirmation links. If an invitation expires before it's accepted, send the user a new invite. + + + ## Resources - [User Management guide](/docs/guides/auth/managing-user-data) diff --git a/apps/docs/content/guides/cron/quickstart.mdx b/apps/docs/content/guides/cron/quickstart.mdx index 9ec8b35a846..6c6b369c8e3 100644 --- a/apps/docs/content/guides/cron/quickstart.mdx +++ b/apps/docs/content/guides/cron/quickstart.mdx @@ -40,14 +40,12 @@ select cron.schedule('permanent-cron-job-name', '30 seconds', 'CALL do_something -
- @@ -68,7 +66,6 @@ select cron.schedule('permanent-cron-job-name', '30 seconds', 'CALL do_something -
diff --git a/apps/docs/content/guides/database/connecting-to-postgres.mdx b/apps/docs/content/guides/database/connecting-to-postgres.mdx index b1ee6e35da7..f18555a9a9f 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 @@ -315,14 +315,18 @@ Because the dedicated pooler is hosted on the same machine as your database, it See the [connection method matrix](#how-to-connect-to-your-postgres-databases) at the top of this page for a quick reference, or follow the decision flow in the diagram below to choose the right option for your environment. -Decision tree diagram showing when to connect directly to Postgres or use a connection pooler. B[Persistent Backend] + A --> C[Serverless / Edge] + B --> D{IPv6 Supported?
IPv4 Add-on?} + B --> E{IPv4 Needed?} + C --> H{IPv6 Supported?
IPv4 Add-on?} + C --> I{IPv4 Needed?} + D --> F[Use Direct Connection] + E --> G[Use Supavisor Session Mode] + H --> J[Use Dedicated Pooler PgBouncer Pro] + I --> K[Use Supavisor Transaction Mode] +``` -/> +The decision depends on where you connect from. For a **persistent backend**, use a direct connection if you can reach the database over IPv6 (or have the IPv4 add-on); otherwise use Supavisor in session mode. For **serverless or edge** environments, use the dedicated pooler (PgBouncer, Pro plan) when IPv6 or the IPv4 add-on is available, or Supavisor in transaction mode when you need IPv4. diff --git a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx index 24591674340..87a78a6f926 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx @@ -35,7 +35,7 @@ Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integra passHref > - Simple Next.js template that uses Supabase as the database and Kysely as the query builder. + Basic Next.js template that uses Supabase as the database and Kysely as the query builder.
diff --git a/apps/docs/content/guides/database/custom-postgres-config.mdx b/apps/docs/content/guides/database/custom-postgres-config.mdx index 4a2c30b7db6..b6a02132e20 100644 --- a/apps/docs/content/guides/database/custom-postgres-config.mdx +++ b/apps/docs/content/guides/database/custom-postgres-config.mdx @@ -119,35 +119,43 @@ Parameters marked with **Restart: Yes** cause the CLI to automatically restart y -Use the examples below with `supabase --experimental --project-ref postgres-config update`: +Use the examples below with `supabase postgres-config update --project-ref --experimental`: -| Parameter | Type | Restart | Example | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------- | --------------------------------------------- | -| [checkpoint_timeout](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-CHECKPOINT-TIMEOUT) | CLI only | No | `--config checkpoint_timeout=15min` | -| [effective_cache_size](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE) | CLI + SQL | No | `--config effective_cache_size=8GB` | -| [hot_standby_feedback](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-HOT-STANDBY-FEEDBACK) | CLI only | No | `--config hot_standby_feedback=true` | -| [logical_decoding_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-LOGICAL-DECODING-WORK-MEM) | CLI + SQL | No | `--config logical_decoding_work_mem=128MB` | -| [maintenance_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM) | CLI + SQL | No | `--config maintenance_work_mem=512MB` | -| [max_connections](https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS) (Be aware of [these considerations](/docs/guides/troubleshooting/how-to-change-max-database-connections-_BQ8P5)) | CLI only | Yes | `--config max_connections=200` | -| [max_locks_per_transaction](https://www.postgresql.org/docs/current/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION) | CLI only | Yes | `--config max_locks_per_transaction=128` | -| [max_parallel_maintenance_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-MAINTENANCE-WORKERS) | CLI + SQL | No | `--config max_parallel_maintenance_workers=2` | -| [max_parallel_workers_per_gather](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS-PER-GATHER) | CLI + SQL | No | `--config max_parallel_workers_per_gather=2` | -| [max_parallel_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS) | CLI + SQL | No | `--config max_parallel_workers=4` | -| [max_replication_slots](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-REPLICATION-SLOTS) | CLI only | Yes | `--config max_replication_slots=10` | -| [max_slot_wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-SLOT-WAL-KEEP-SIZE) | CLI only | No | `--config max_slot_wal_keep_size=4GB` | -| [max_standby_archive_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-ARCHIVE-DELAY) | CLI only | No | `--config max_standby_archive_delay=30s` | -| [max_standby_streaming_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-STREAMING-DELAY) | CLI only | No | `--config max_standby_streaming_delay=30s` | -| [max_wal_size](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-MAX-WAL-SIZE) | CLI only | No | `--config max_wal_size=2GB` | -| [max_wal_senders](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-WAL-SENDERS) | CLI only | Yes | `--config max_wal_senders=10` | -| [max_worker_processes](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES) | CLI only | Yes | `--config max_worker_processes=8` | -| [session_replication_role](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE) | CLI only | No | `--config session_replication_role=replica` | -| [shared_buffers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS) | CLI only | Yes | `--config shared_buffers=256MB` | -| [statement_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT) | CLI + SQL | No | `--config statement_timeout=60s` | -| [track_activity_query_size](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE) | CLI only | Yes | `--config track_activity_query_size=2048B` | -| [track_commit_timestamp](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP) | CLI only | Yes | `--config track_commit_timestamp=true` | -| [wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE) | CLI only | No | `--config wal_keep_size=1GB` | -| [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) | CLI only | No | `--config wal_sender_timeout=60s` | -| [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM) | CLI + SQL | No | `--config work_mem=64MB` | +| Parameter | Type | Restart | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------- | ----------------------------------------------- | +| [checkpoint_timeout](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-CHECKPOINT-TIMEOUT) | CLI only | No | `--config checkpoint_timeout=15min` | +| [effective_cache_size](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-EFFECTIVE-CACHE-SIZE) | CLI + SQL | No | `--config effective_cache_size=8GB` | +| [hot_standby_feedback](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-HOT-STANDBY-FEEDBACK) | CLI only | No | `--config hot_standby_feedback=true` | +| [logical_decoding_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-LOGICAL-DECODING-WORK-MEM) | CLI + SQL | No | `--config logical_decoding_work_mem=128MB` | +| [maintenance_work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAINTENANCE-WORK-MEM) | CLI + SQL | No | `--config maintenance_work_mem=512MB` | +| [max_connections](https://www.postgresql.org/docs/current/runtime-config-connection.html#GUC-MAX-CONNECTIONS) (Be aware of [these considerations](/docs/guides/troubleshooting/how-to-change-max-database-connections-_BQ8P5)) | CLI only | Yes | `--config max_connections=200` | +| [max_locks_per_transaction](https://www.postgresql.org/docs/current/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION) | CLI only | Yes | `--config max_locks_per_transaction=128` | +| [max_logical_replication_workers](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-LOGICAL-REPLICATION-WORKERS) | CLI only | Yes | `--config max_logical_replication_workers=10` | +| [max_parallel_maintenance_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-MAINTENANCE-WORKERS) | CLI + SQL | No | `--config max_parallel_maintenance_workers=2` | +| [max_parallel_workers_per_gather](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS-PER-GATHER) | CLI + SQL | No | `--config max_parallel_workers_per_gather=2` | +| [max_parallel_workers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-PARALLEL-WORKERS) | CLI + SQL | No | `--config max_parallel_workers=4` | +| [max_replication_slots](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-REPLICATION-SLOTS) | CLI only | Yes | `--config max_replication_slots=10` | +| [max_slot_wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-SLOT-WAL-KEEP-SIZE) | CLI only | No | `--config max_slot_wal_keep_size=4GB` | +| [max_standby_archive_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-ARCHIVE-DELAY) | CLI only | No | `--config max_standby_archive_delay=30s` | +| [max_standby_streaming_delay](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-STANDBY-STREAMING-DELAY) | CLI only | No | `--config max_standby_streaming_delay=30s` | +| [max_sync_workers_per_subscription](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-SYNC-WORKERS-PER-SUBSCRIPTION) | CLI only | No | `--config max_sync_workers_per_subscription=10` | +| [max_wal_size](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-MAX-WAL-SIZE) | CLI only | No | `--config max_wal_size=2GB` | +| [max_wal_senders](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-MAX-WAL-SENDERS) | CLI only | Yes | `--config max_wal_senders=10` | +| [max_worker_processes](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES) | CLI only | Yes | `--config max_worker_processes=8` | +| [session_replication_role](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE) | CLI only | No | `--config session_replication_role=replica` | +| [shared_buffers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS) | CLI only | Yes | `--config shared_buffers=256MB` | +| [statement_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT) | CLI + SQL | No | `--config statement_timeout=60s` | +| [track_activity_query_size](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE) | CLI only | Yes | `--config track_activity_query_size=2048B` | +| [track_commit_timestamp](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP) | CLI only | Yes | `--config track_commit_timestamp=true` | +| [wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE) | CLI only | No | `--config wal_keep_size=1GB` | +| [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) | CLI only | No | `--config wal_sender_timeout=60s` | +| [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM) | CLI + SQL | No | `--config work_mem=64MB` | + +#### Management API only parameters + +Some Postgres settings are configurable through the [Management API](/docs/reference/api/v1-update-postgres-config) but not the CLI. These include logging settings such as `log_connections`. See [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) for details. + +`cron.log_statement` (which logs each [pg_cron](/docs/guides/database/extensions/pg_cron) job statement before it runs) is also Management API only. It is a `postmaster`-context parameter, so updating it **requires a database restart** to take effect. #### Managing Postgres configuration with the CLI @@ -159,26 +167,26 @@ To start: To update Postgres configurations, use the [`postgres config`](/docs/reference/cli/supabase-postgres-config) command: ```bash -supabase --experimental \ +supabase postgres-config update --config shared_buffers=250MB \ --project-ref \ -postgres-config update --config shared_buffers=250MB +--experimental ``` By default, the CLI will merge any provided config overrides with any existing ones. The `--replace-existing-overrides` flag can be used to instead force all existing overrides to be replaced with the ones being provided: ```bash -supabase --experimental \ +supabase postgres-config update --config max_parallel_workers=3 \ +--replace-existing-overrides \ --project-ref \ -postgres-config update --config max_parallel_workers=3 \ ---replace-existing-overrides +--experimental ``` To delete specific configuration overrides, use the `postgres-config delete` command: ```bash -supabase --experimental \ +supabase postgres-config delete --config shared_buffers,work_mem \ --project-ref \ -postgres-config delete --config shared_buffers,work_mem +--experimental ``` By default, CLI v2 (≥ 2.0.0) checks the parameter’s context and requests the correct action (reload or restart): @@ -212,14 +220,14 @@ 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). ```bash -supabase --experimental \ +supabase postgres-config delete --config shared_buffers --no-restart \ --project-ref \ -postgres-config delete --config shared_buffers --no-restart +--experimental ``` ### Resetting to default config diff --git a/apps/docs/content/guides/database/drizzle.mdx b/apps/docs/content/guides/database/drizzle.mdx index 852e0d356f4..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 + 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 = URL.parse(connectionString)! + 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/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..6863634ff8b 100644 --- a/apps/docs/content/guides/database/extensions/hypopg.mdx +++ b/apps/docs/content/guides/database/extensions/hypopg.mdx @@ -6,7 +6,7 @@ description: 'Quickly check if an index can be used without creating it.' `HypoPG` is Postgres extension for creating hypothetical/virtual indexes. HypoPG allows users to rapidly create hypothetical/virtual indexes that have no resource cost (CPU, disk, memory) that are visible to the Postgres query planner. -The motivation for HypoPG is to allow users to quickly search for an index to improve a slow query without consuming server resources or waiting for them to build. +The motivation for HypoPG is to allow users to search for an index to improve a slow query without consuming server resources or waiting for them to build. ## Enable the extension @@ -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/pgaudit.mdx b/apps/docs/content/guides/database/extensions/pgaudit.mdx index 6ec31eec88c..1f6ff73921a 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 ''; @@ -193,7 +193,7 @@ You can then assign the role to monitor only approved object events, such as `se grant select on random_table to "some_audit_role"; ``` -With this privilege granted, PGAudit will record all select statements that reference the `random_table`, regardless of _who_ or _what_ actually initiated the event. All assignable privileges can be viewed in the [Postgres documentation](https://www.postgresql.org/docs/current/ddl-priv.html). +With this privilege granted, PGAudit will record all select statements that reference the `random_table`, regardless of _who_ or _what_ initiated the event. All assignable privileges can be viewed in the [Postgres documentation](https://www.postgresql.org/docs/current/ddl-priv.html). If you would no longer like to use object logging, you will need to unassign the `pgaudit.role` variable: diff --git a/apps/docs/content/guides/database/extensions/pgroonga.mdx b/apps/docs/content/guides/database/extensions/pgroonga.mdx index 68bdb78ea77..3e8f135c000 100644 --- a/apps/docs/content/guides/database/extensions/pgroonga.mdx +++ b/apps/docs/content/guides/database/extensions/pgroonga.mdx @@ -124,7 +124,7 @@ id | content ### Match all search words -To find all memos where content contains BOTH of the words `postgres` and `pgroonga`, we can just use space to separate each words: +To find all memos where content contains BOTH of the words `postgres` and `pgroonga`, we can use space to separate each words: {/* prettier-ignore */} ```sql 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/plv8.mdx b/apps/docs/content/guides/database/extensions/plv8.mdx index 190cdcf6d24..219f98acc81 100644 --- a/apps/docs/content/guides/database/extensions/plv8.mdx +++ b/apps/docs/content/guides/database/extensions/plv8.mdx @@ -55,7 +55,7 @@ Procedural languages are automatically installed within `pg_catalog`, so you don ## Create `plv8` functions -Functions written in `plv8` are written just like any other Postgres functions, only +Functions written in `plv8` are written like any other Postgres functions, only with the `language` identifier set to `plv8`. ```sql diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 599f65be591..c02e5f9c9ae 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 ( @@ -192,7 +192,7 @@ val data = supabase.from("restaurants").insert(listOf(
-Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is when inserting data from the client library, there is no comma between the two values, just a single space. +Notice the order in which you pass the latitude and longitude. Longitude comes first, and is because longitude represents the x-axis of the location. Another thing to watch for is when inserting data from the client library, there is no comma between the two values, only a single space. At this point, if you go into your Supabase dashboard and look at the data, you will notice that the value of the `location` column looks something like this. @@ -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/extensions/wrappers/overview.mdx b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx index 5695f1aa007..867129be896 100644 --- a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx +++ b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx @@ -65,7 +65,7 @@ where ts > (now() - interval '1 DAY'); This approach provides several benefits: -1. **Simplicity:** the Wrappers API is just SQL, so data engineers don't need to learn new tools and languages. +1. **Simplicity:** the Wrappers API is SQL, so data engineers don't need to learn new tools and languages. 1. **Save on time:** avoid setting up additional data pipelines. 1. **Save on Data Engineering costs:** less infrastructure to be managed. 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/import-data.mdx b/apps/docs/content/guides/database/import-data.mdx index ca2e330b508..0b0262bdc7a 100644 --- a/apps/docs/content/guides/database/import-data.mdx +++ b/apps/docs/content/guides/database/import-data.mdx @@ -5,7 +5,7 @@ title: 'Import data into Supabase' You can import data into Supabase in multiple ways. The best method depends on your data size and app requirements. -If you're working with small datasets in development, you can experiment quickly using CSV import in the Supabase dashboard. If you're working with a large dataset in production, you should plan your data import to minimize app latency and ensure data integrity. +If you're working with small datasets in development, you can experiment with CSV import in the Supabase dashboard. If you're working with a large dataset in production, you should plan your data import to minimize app latency and ensure data integrity. ## How to import data into Supabase diff --git a/apps/docs/content/guides/database/inspect.mdx b/apps/docs/content/guides/database/inspect.mdx index cce91f7b3b5..8d01413792c 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. @@ -47,7 +47,7 @@ Most inspection commands are Postgres agnostic. You can run inspection routines For example you can connect to your local Postgres instance: ``` -supabase --db-url postgresql://postgres:postgres@localhost:5432/postgres inspect db bloat +supabase inspect db bloat --db-url postgresql://postgres:postgres@localhost:5432/postgres ``` ### Connect to a Supabase instance @@ -250,9 +250,9 @@ Postgres has built in tooling to help you optimize poorly performing queries. Yo explain analyze ; ``` -When you include `analyze` in the explain statement, the database attempts to execute the query and provides a detailed query plan along with actual execution times. So, be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will actually run, and could have unintended side-effects. +When you include `analyze` in the explain statement, the database attempts to execute the query and provides a detailed query plan along with actual execution times. So, be careful using `explain analyze` with `insert`/`update`/`delete` queries, because the query will run, and could have unintended side-effects. -If you run just `explain` without the `analyze` keyword, the database will only perform query planning without actually executing the query. This approach can be beneficial when you want to inspect the query plan without affecting the database or if you encounter timeouts in your queries. +If you run `explain` without the `analyze` keyword, the database will only perform query planning without executing the query. This approach can be beneficial when you want to inspect the query plan without affecting the database or if you encounter timeouts in your queries. Using the query plan analyzer to optimize your queries is a large topic, with a number of online resources available: 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`: - -You can work with a project's database in several ways: +Work with your project's database in the following ways: - 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. +- Programmatically using a variety of different methods. -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. + - - -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. - -## Get started - -If you're new to the database section, these are the pages to read first: - -- **[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. - -## Going further - -Once you've covered the basics, these guides help with other use cases and features: - -- **[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..0f6164c4922 100644 --- a/apps/docs/content/guides/database/partitions.mdx +++ b/apps/docs/content/guides/database/partitions.mdx @@ -6,8 +6,10 @@ description: 'Organizing tables into partitions in Postgres.' Table partitioning is a technique that allows you to divide a large table into smaller, more manageable parts called “partitions”. +The diagram below shows a single large table split into several smaller partitions, each holding a subset of its rows. + multi database @@ -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` @@ -31,7 +31,7 @@ For example, let's assume you have a `posts` table with the following columns: - `created_at` - `updated_at` -You can restrict updates to just the user who created it using [RLS](/docs/guides/auth#row-level-security), with the following policy: +You can restrict updates to the user who created it using [RLS](/docs/guides/auth#row-level-security), with the following policy: ```sql create policy "Allow update for owners" on posts for @@ -66,7 +66,7 @@ update (title, content) on table public.posts to authenticated; ``` -In the above example, we are revoking the table-level `UPDATE` privilege from the `authenticated` role and granting a column-level `UPDATE` privilege on just the `title` and `content` columns. +In the above example, we are revoking the table-level `UPDATE` privilege from the `authenticated` role and granting a column-level `UPDATE` privilege on the `title` and `content` columns. If we want to restrict access to updating the `title` column: diff --git a/apps/docs/content/guides/database/postgres/data-deletion.mdx b/apps/docs/content/guides/database/postgres/data-deletion.mdx index a27ac37074c..eecfd8f66cb 100644 --- a/apps/docs/content/guides/database/postgres/data-deletion.mdx +++ b/apps/docs/content/guides/database/postgres/data-deletion.mdx @@ -47,7 +47,7 @@ delete from logs where created_at < now() - interval '90 days'; ``` -This acquires a `ROW EXCLUSIVE` lock on the table, which still allows other `SELECT`, `INSERT`, `UPDATE`, and `DELETE` statements to run concurrently. For small row counts, the operation completes quickly and has minimal impact. +This acquires a `ROW EXCLUSIVE` lock on the table, which still allows other `SELECT`, `INSERT`, `UPDATE`, and `DELETE` statements to run concurrently. For small row counts, the operation completes with minimal impact. ### Large deletes @@ -188,7 +188,7 @@ If autovacuum is not keeping up, you can trigger a manual vacuum: vacuum (verbose) logs; ``` -For reclaiming disk space (not just marking tuples as reusable), use `VACUUM FULL` — but be aware this rewrites the entire table and takes an `ACCESS EXCLUSIVE` lock: +To reclaim disk space rather than only marking tuples as reusable, use `VACUUM FULL`. Note that this rewrites the entire table and takes an `ACCESS EXCLUSIVE` lock: ```sql -- This locks the table for the duration — use during maintenance windows only 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/event-triggers.mdx b/apps/docs/content/guides/database/postgres/event-triggers.mdx index 78da36e91e9..682b7d042a5 100644 --- a/apps/docs/content/guides/database/postgres/event-triggers.mdx +++ b/apps/docs/content/guides/database/postgres/event-triggers.mdx @@ -61,10 +61,10 @@ See how to [auto enable RLS for new tables](/docs/guides/database/postgres/row-l Event triggers can be triggered on: -- `ddl_command_start` - occurs just before a DDL command for almost all objects within a schema -- `ddl_command_end` - occurs just after a DDL command for almost all objects within a schema -- `sql_drop` - occurs just before `ddl_command_end` for any DDL commands that `DROP` a database object (note that altering a table can cause it to be dropped) -- `table_rewrite` - occurs just before a table is rewritten using the `ALTER TABLE` command +- `ddl_command_start` - occurs before a DDL command for almost all objects within a schema +- `ddl_command_end` - occurs after a DDL command for almost all objects within a schema +- `sql_drop` - occurs before `ddl_command_end` for any DDL commands that `DROP` a database object (note that altering a table can cause it to be dropped) +- `table_rewrite` - occurs before a table is rewritten using the `ALTER TABLE` command diff --git a/apps/docs/content/guides/database/postgres/indexes.mdx b/apps/docs/content/guides/database/postgres/indexes.mdx index d0b36b77bf0..ec1c5a24724 100644 --- a/apps/docs/content/guides/database/postgres/indexes.mdx +++ b/apps/docs/content/guides/database/postgres/indexes.mdx @@ -5,7 +5,7 @@ footerHelpType: 'postgres' tocVideo: 'bBu_V8CfWgM' --- -An index makes your Postgres queries faster. The index is like a "table of contents" for your data - a reference list which allows queries to quickly locate a row in a given table without needing to scan the entire table (which in large tables can take a long time). +An index makes your Postgres queries faster. The index is like a "table of contents" for your data - a reference list which allows queries to locate a row in a given table without needing to scan the entire table (which in large tables can take a long time). Indexes can be structured in a few different ways. The type of index chosen depends on the values you are indexing. By far the most common index type, and the default in Postgres, is the B-Tree. A B-Tree is the generalized form of a binary search tree, where nodes can have more than two children. @@ -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); @@ -67,7 +67,7 @@ Luckily Postgres provides us with `create index concurrently` which prevents blo -Here is a simplified diagram of the index we just created (note that in practice, nodes actually have more than two children). +Here is a simplified diagram of the index we created (note that in practice, nodes have more than two children). B-Tree index example in Postgres diff --git a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx index ff3757e0eab..b9e9ff75b14 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. +- **Insufficient pool size**: The Supavisor pooler does not have enough connections available to 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,46 +85,46 @@ 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. +- **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 one, is often sufficient. In serverless setups, begin with `connection_limit=1`, increasing cautiously if needed to avoid maxing out connections. - **Increase pool size**: If you are connecting with Supavisor, try increasing the pool size in the [Database Settings](/dashboard/project/_/database/settings). - **Disconnect appropriately**: Close Prisma connections when they are no longer needed. - **Decrease query time**: Reduce query complexity or add [strategic indexes](/docs/guides/database/postgres/indexes) to your tables to speed up queries. @@ -132,15 +132,15 @@ 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, list the relevant schemas on the `datasource` block in your `schema.prisma` file. diff --git a/apps/docs/content/guides/database/replication.mdx b/apps/docs/content/guides/database/replication.mdx index 8ff0b26278f..7ad4408b84e 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 (ETL), and manual replication.' +description: 'Compare read replicas, Supabase Pipelines, 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 a destination 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 replication pipeline to a destination system, or full control over your own logical replication setup. ### Read replicas @@ -26,21 +26,23 @@ Read replicas are additional Supabase Postgres databases kept in sync with your - [Set up read replicas](/docs/guides/platform/read-replicas) -### External replication (ETL) [#external-replication] +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} + +### Pipelines -External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. +Supabase Pipelines is currently in private alpha. Private alpha features can be unstable and may introduce breaking changes while we evaluate the product direction, refine the feature set, and incorporate customer feedback. -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. +Pipelines is a managed CDC feature for creating replication pipelines from Supabase Postgres to destination systems. It uses Postgres logical replication under the hood with the open-source [Supabase ETL engine](https://github.com/supabase/etl). A destination is where your replicated data is stored; the pipeline is the managed process that continuously streams database changes to that destination. -- [Set up external replication (ETL)](/docs/guides/database/replication/external-replication-setup) +- [Set up Pipelines](/docs/guides/database/replication/pipelines) #### Supported destinations -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. +Pipelines 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 +50,7 @@ External replication (ETL) currently supports BigQuery as the managed destinatio ### Manual 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. +Manual replication uses the same underlying Postgres logical replication features as Pipelines, 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) @@ -78,7 +80,7 @@ When setting up logical replication, three key components are involved: - `publication` - A set of tables on your primary database that will be `published` - `replication slot` - A slot used for replicating the data from a single publication. The slot, when created, will specify the output format of the changes -- `subscription` - A subscription is created from an external system (i.e. another Postgres database) and must specify the name of the `publication`. If you do not specify a replication slot, one is automatically created +- `subscription` - A subscription is created from an external system (that is, another Postgres database) and must specify the name of the `publication`. If you do not specify a replication slot, one is automatically created ## Logical replication output format @@ -86,7 +88,7 @@ Logical replication is typically output in two forms, `pgoutput` and `wal2json`. ## Logical replication configuration -When using logical replication, Postgres keeps WAL files around for longer than it otherwise needs them. If the files are removed too quickly, then your `replication slot` can become inactive or lost if the database receives a large number of changes in a short time. +When using logical replication, Postgres keeps WAL files around for longer than it otherwise needs them. If the files are removed too soon, then your `replication slot` can become inactive or lost if the database receives a large number of changes in a short time. In order to mitigate this, Postgres has many options and settings that can be [tweaked](/docs/guides/database/custom-postgres-config) to manage the WAL usage effectively. Not all of these settings are user configurable as they can impact the stability of your database. For those that are, these should be considered as advanced configuration and not changed without understanding that they can cause additional disk space and resources to be used, as well as incur additional costs. diff --git a/apps/docs/content/guides/database/replication/bigquery.mdx b/apps/docs/content/guides/database/replication/bigquery.mdx index 12f1ae14fc9..ca96330f844 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 (ETL) destination.' +description: 'Configure BigQuery as a Supabase Pipelines destination.' subtitle: 'Replicate Supabase Postgres tables to BigQuery.' sidebar_label: 'BigQuery' --- -External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. +Supabase Pipelines is currently in private alpha. Private alpha features can be unstable and may introduce breaking changes while we evaluate the product direction, refine the feature set, and incorporate customer feedback. @@ -75,7 +75,7 @@ Required permissions: | **Connection pool size** | `4` connections | Size of the BigQuery Storage Write API connection pool. More connections allow more parallel writes, but consume more resources. | | **Maximum staleness** | No staleness limit | Maximum allowed age, in minutes, for BigQuery cached metadata before reading base tables. Lower values improve freshness. Higher values can reduce query cost and latency. | -6. Click **Create and start** to begin replication +6. Click **Create and start pipeline** to begin replication Your replication pipeline now starts copying data from your database to BigQuery. @@ -90,7 +90,7 @@ 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. Supabase ETL declares those columns as the BigQuery destination primary key so BigQuery change data capture (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. Pipelines 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 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. @@ -104,7 +104,7 @@ Source tables must also use a BigQuery-compatible Postgres `REPLICA IDENTITY` se | `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 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/pipelines-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. 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`. @@ -145,7 +145,7 @@ This structure handles table truncations while maintaining query compatibility. ## Schema change support -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. +Schema change support for BigQuery is currently in beta. Pipelines supports a limited set of schema changes for BigQuery while the feature is developed further. Supported schema changes: @@ -162,7 +162,7 @@ Unsupported or limited schema changes: - Filling existing rows for `ADD COLUMN ... DEFAULT` - Unsupported default expressions -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. +BigQuery requires added columns to be nullable. When a replicated `ADD COLUMN` includes a default, Pipelines 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. 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 8d5076fedc2..e2d414c8c62 100644 --- a/apps/docs/content/guides/database/replication/manual-replication-setup.mdx +++ b/apps/docs/content/guides/database/replication/manual-replication-setup.mdx @@ -6,7 +6,7 @@ subtitle: 'Set up replication with Airbyte, Estuary, Fivetran, and other tools.' sidebar_label: 'Setting up' --- -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 covers setting up **manual logical replication** using your own tools. If you prefer a managed solution, read [Set up Pipelines](/docs/guides/database/replication/pipelines) instead. diff --git a/apps/docs/content/guides/database/replication/external-replication-faq.mdx b/apps/docs/content/guides/database/replication/pipelines-faq.mdx similarity index 65% rename from apps/docs/content/guides/database/replication/external-replication-faq.mdx rename to apps/docs/content/guides/database/replication/pipelines-faq.mdx index 6c56a465e64..f8268695983 100644 --- a/apps/docs/content/guides/database/replication/external-replication-faq.mdx +++ b/apps/docs/content/guides/database/replication/pipelines-faq.mdx @@ -1,26 +1,36 @@ --- -id: 'external-replication-faq' -title: 'External replication (ETL) FAQ' -description: 'Frequently asked questions about external replication (ETL).' -subtitle: 'Common questions and answers about external replication (ETL).' +id: 'pipelines-faq' +title: 'Pipelines FAQ' +description: 'Frequently asked questions about Supabase Pipelines.' +subtitle: 'Common questions and answers about managed replication pipelines.' sidebar_label: 'FAQ' --- -External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. +Supabase Pipelines is currently in private alpha. Private alpha features can be unstable and may introduce breaking changes while we evaluate the product direction, refine the feature set, and incorporate customer feedback. ## What destinations are supported? -External replication (ETL) currently supports **BigQuery** as the managed destination. See the [BigQuery destination guide](/docs/guides/database/replication/bigquery) for configuration details. +Pipelines 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. "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. +We are working on new destinations. 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 (ETL)? [#which-plans-support-external-replication] +## Does the destination's region affect performance? -External replication (ETL) is available on the Pro, Team, and Enterprise plans. +Yes. The further apart your source database, the pipeline, and your destination are, the more network latency is added to replication, which increases replication lag and reduces throughput. + +Pipelines run out of **eu-central-1 (Frankfurt)**. For the best performance, place both your source project and your destination as close to this region as possible. + +If you can only optimize one side, prioritize placing your **destination** close to the pipeline's region. Replicated data continuously streams out to the destination, so latency on that leg has a bigger impact on overall replication lag than latency between the pipeline and the source. + +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} + +## Which plans support Pipelines? + +Pipelines is available on the Pro, Team, and Enterprise plans. {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} @@ -28,13 +38,15 @@ External replication (ETL) is available on the Pro, Team, and Enterprise plans. We are currently working on a new Supabase Warehouse product designed to address the limitations of the previous Analytics Buckets. Our goal is to build a solution we can confidently stand behind, rather than continuing to support an approach that does not meet the quality and flexibility we want for our users. -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. +As a result, replication into Analytics Buckets via Pipelines is no longer supported. Right now, **BigQuery** is the only supported managed destination, and we are actively working on expanding capabilities. -## What does external replication (ETL) install in the database? [#what-does-external-replication-install-in-the-database] +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} -When you enable external replication (ETL), Supabase installs database objects that help Supabase ETL track replication state and support schema changes: +## What does Pipelines install in the database? -- An event trigger that runs on every `ALTER TABLE` statement. Supabase ETL uses this to support schema change handling. +When you enable Pipelines, Supabase installs database objects that help track replication state and support schema changes: + +- An event trigger that runs on every `ALTER TABLE` statement. Supabase 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. @@ -51,15 +63,17 @@ Supported schema changes: - Dropping a `NOT NULL` constraint - Setting or dropping supported column default metadata -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. +Pipelines 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 (ETL)? [#what-happens-when-you-disable-external-replication] +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} -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. +## What happens when you disable Pipelines? -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 Pipelines 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) stops Supabase from managing replication for the project. It does not delete tables or data that were already written to your destination. +To remove Pipelines from a project, delete all replication pipelines first. After all pipelines are deleted, the disable action becomes available. For the Dashboard steps, see [Disabling Pipelines](/docs/guides/database/replication/pipelines#disabling-pipelines). + +Disabling Pipelines 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? @@ -75,11 +89,11 @@ Custom data types replicate as strings. Check that your destination can interpre ## 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. +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. Pipelines 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. +See [Partitioned tables](/docs/guides/database/replication/pipelines#partitioned-tables) for examples and the full behavior. ## How does replica identity affect updates and deletes? @@ -131,24 +145,38 @@ After changing replica identity, restart each pipeline that includes the affecte ## Why aren't publication changes reflected after adding or removing tables? -After modifying your Postgres publication, you must restart the replication pipeline for changes to take effect. See [Adding or removing tables](/docs/guides/database/replication/external-replication-setup#adding-or-removing-tables) for instructions. +After modifying your Postgres publication, you must restart the replication pipeline for changes to take effect. See [Adding or removing tables](/docs/guides/database/replication/pipelines#adding-or-removing-tables) for instructions. ## Why is a pipeline in failed state? Pipeline failures occur during the streaming phase when an error happens while replicating live data. This prevents data loss. To recover: 1. Check the error message by hovering over the **Failed** status -2. Click **View status** for detailed information +2. Click **View pipeline** for detailed information 3. Fix the underlying issue (e.g., schema mismatches, destination connectivity) 4. Restart the pipeline -See [Handling errors](/docs/guides/database/replication/external-replication-monitoring#handling-errors) for more details. +See [Handling errors](/docs/guides/database/replication/pipelines-monitoring#handling-errors) for more details. + +## Why was the pipeline stopped? + +Supabase may automatically stop a pipeline if it encounters too many errors in a short period of time. This protects your source database, your destination, and the Pipelines system from a pipeline repeatedly retrying a failing operation. + +Automatic stopping is a safety mechanism. It prevents repeated pipeline errors from consuming resources or causing wider instability while preserving the pipeline's replication position so you can investigate the underlying issue. + +To recover: + +1. Check the pipeline error details and replication logs +2. Fix the underlying issue, such as destination connectivity, schema mismatches, permissions, or rate limits +3. Restart the pipeline manually from the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard + +If the same error continues after restart, the pipeline may stop again. Review [Monitor pipeline status](/docs/guides/database/replication/pipelines-monitoring) for troubleshooting steps. ## 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. +Open [**Database > Replication**](/dashboard/project/_/database/replication), click **View pipeline**, and check **Waiting to sync**, **Room before pausing**, **Last check-in**, **Connected**, and **Slot status**. See [Dealing with replication lag](/docs/guides/database/replication/pipelines-monitoring#dealing-with-replication-lag) for the full investigation and response flow. ## What does a `Lost` slot status mean? @@ -156,26 +184,26 @@ Open [**Database > Replication**](/dashboard/project/_/database/replication), cl 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. +See [Slot statuses](/docs/guides/database/replication/pipelines-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. +Table errors occur during the copy phase. To recover, click **View pipeline**, find the affected table, and reset the table state. This will restart the table copy from the beginning. ## How to verify replication is working Check the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard: 1. Verify your pipeline shows **Running** status -2. Click **View status** to check table states +2. Click **View pipeline** to check table states 3. Ensure all tables show **Live** state (actively replicating) 4. Monitor replication lag metrics -See the [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring) for comprehensive monitoring instructions. +See [Monitor pipeline status](/docs/guides/database/replication/pipelines-monitoring) for comprehensive monitoring instructions. ## How to stop or pause replication -You can manage your pipeline using the actions menu in the destinations list. See [Managing your pipeline](/docs/guides/database/replication/external-replication-setup#managing-your-pipeline) for details on available actions. +You can manage your pipeline using the actions menu in the destinations list. See [Managing your pipeline](/docs/guides/database/replication/pipelines#managing-your-pipeline) for details on available actions. @@ -185,13 +213,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 (ETL) stops any running pipelines and does not automatically resume them after the project is restarted. +If your project becomes inactive, Pipelines 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 (ETL) pipelines for that project are deleted. +When a project is downgraded to the Free Plan, all replication pipelines created with Pipelines for that project are deleted. ## What happens if a table is deleted at the destination? @@ -245,6 +273,6 @@ Navigate to the [**Logs > Replication**](/dashboard/project/_/logs/explorer) sec If you need assistance: -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) +1. Check [Set up Pipelines](/docs/guides/database/replication/pipelines) and [Monitor pipeline status](/docs/guides/database/replication/pipelines-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/pipelines-monitoring.mdx similarity index 89% rename from apps/docs/content/guides/database/replication/external-replication-monitoring.mdx rename to apps/docs/content/guides/database/replication/pipelines-monitoring.mdx index 789607ce586..f466c8108bd 100644 --- a/apps/docs/content/guides/database/replication/external-replication-monitoring.mdx +++ b/apps/docs/content/guides/database/replication/pipelines-monitoring.mdx @@ -1,18 +1,18 @@ --- -id: 'external-replication-monitoring' -title: 'External replication (ETL) monitoring' -description: 'Monitor the status and health of your external replication (ETL) pipelines.' +id: 'pipelines-monitoring' +title: 'Monitor pipeline status' +description: 'Monitor the status and health of pipelines created with Supabase Pipelines.' subtitle: 'Track replication status, view logs, and troubleshoot issues.' sidebar_label: 'Monitoring' --- -External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. +Supabase Pipelines is currently in private alpha. Private alpha features can be unstable and may introduce breaking changes while we evaluate the product direction, refine the feature set, and incorporate customer feedback. -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. +After setting up Pipelines, 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 @@ -43,10 +43,10 @@ Each destination shows its pipeline in one of these states: ### Viewing detailed pipeline metrics -For detailed information about a specific pipeline, click **View status** on the destination. This opens the pipeline status page where you can monitor replication performance and table states. +For detailed information about a specific pipeline, click **View pipeline** on the destination. This opens the pipeline status page where you can monitor replication performance and table states. Pipeline Status View 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**. +2. Click **View pipeline** 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. @@ -136,7 +136,7 @@ After the affected table finishes copying and catches up, the temporary slot is | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **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. | +| **Unreserved** | Act soon. 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. | @@ -166,7 +166,7 @@ height={2146} **Viewing table error details:** -1. Click **View status** on your destination +1. Click **View pipeline** on your destination 2. Check the table states section to identify tables in **Error** state 3. Review the error message for that specific table @@ -185,7 +185,7 @@ When a pipeline error occurs, you'll receive an email notification immediately. Pipeline Error Details Replication**](/dashboard/project/_/logs/explorer) section of the Dashboard for detailed error logs **Recovering from pipeline errors:** @@ -233,7 +233,7 @@ Logs contain diagnostic information that may be too technical for most users. If 1. Navigate to the [**Database > Replication**](/dashboard/project/_/database/replication) section of the Dashboard 2. Verify your destination shows a "Running" status -3. Click **View status** to check replication lag and table states +3. Click **View pipeline** to check replication lag and table states 4. Ensure all tables show a "Live" state #### Investigating errors @@ -241,7 +241,7 @@ Logs contain diagnostic information that may be too technical for most users. If If you see a **Failed** status: 1. Hover over the status to see the error summary -2. Click **View status** to see detailed error information +2. Click **View pipeline** to see detailed error information 3. Check table states to identify which tables are affected 4. Navigate to the [**Logs > Replication**](/dashboard/project/_/logs/explorer) section of the Dashboard for full error details 5. For table errors, attempt to reset the affected tables @@ -265,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 (ETL) FAQ](/docs/guides/database/replication/external-replication-faq). +For more troubleshooting tips, see the [Pipelines FAQ](/docs/guides/database/replication/pipelines-faq). ### Next steps -- [Set up external replication (ETL)](/docs/guides/database/replication/external-replication-setup) -- [View external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq) +- [Set up Pipelines](/docs/guides/database/replication/pipelines) +- [View the Pipelines FAQ](/docs/guides/database/replication/pipelines-faq) diff --git a/apps/docs/content/guides/database/replication/external-replication-setup.mdx b/apps/docs/content/guides/database/replication/pipelines.mdx similarity index 74% rename from apps/docs/content/guides/database/replication/external-replication-setup.mdx rename to apps/docs/content/guides/database/replication/pipelines.mdx index 849560ba439..f295c17436c 100644 --- a/apps/docs/content/guides/database/replication/external-replication-setup.mdx +++ b/apps/docs/content/guides/database/replication/pipelines.mdx @@ -1,26 +1,26 @@ --- -id: 'external-replication-setup' -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).' +id: 'pipelines' +title: 'Set up Pipelines' +description: 'Create a managed replication pipeline using Postgres logical replication.' +subtitle: 'Configure publications, destinations, and managed replication pipelines.' sidebar_label: 'Setting up' --- -External replication (ETL) is currently in private alpha. Managed pipelines run through Supabase ETL. Access is limited and features may change. +Supabase Pipelines is currently in private alpha. Private alpha features can be unstable and may introduce breaking changes while we evaluate the product direction, refine the feature set, and incorporate customer feedback. -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. +Pipelines is a managed CDC feature for creating replication pipelines from Supabase Postgres to destination systems. It uses **Postgres logical replication** with the open-source [Supabase ETL engine](https://github.com/supabase/etl). You choose a destination in the Dashboard, and Supabase runs the pipeline that streams database changes to that destination. ## Setup overview -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. +Pipelines requires two main components: a **Postgres publication** (defines what to replicate) and a **destination** (where data is sent). Supabase 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 (ETL)](#step-2-enable-external-replication). +If you already have a Postgres publication set up, you can skip to [Step 2: Enable Pipelines](#step-2-enable-pipelines). @@ -90,7 +90,7 @@ 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. +Pipelines 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 | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | @@ -118,7 +118,7 @@ Use `publish_via_partition_root = true` when you want analytics queries to read 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`. +On Postgres 15 and newer, row filters on partition publications apply during both the initial copy and streaming phases. Pipelines 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`. @@ -133,17 +133,19 @@ 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 (ETL) [#step-2-enable-external-replication] +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} -Before creating an external replication (ETL) pipeline, enable it for your project: +### Step 2: Enable Pipelines + +Before creating a managed replication pipeline, enable Pipelines 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 (ETL) destination, such as **BigQuery** -4. Click **Enable external replication (ETL)** +3. Select a Pipelines destination, such as **BigQuery** +4. Click **Enable Pipelines** Enable external replication (ETL) -For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [external replication (ETL) monitoring guide](/docs/guides/database/replication/external-replication-monitoring). +For comprehensive monitoring instructions including pipeline states, metrics, and logs, see [Monitor pipeline status](/docs/guides/database/replication/pipelines-monitoring). ### Managing your pipeline @@ -213,24 +215,26 @@ You can manage your pipeline from the destinations list using the actions menu. Available actions: -- **Start**: Begin replication for a stopped pipeline +- **Start pipeline**: Begin replication for a stopped pipeline - **Stop**: Pause replication (changes will queue up in the WAL) -- **Restart**: Stop and start the pipeline (required after publication changes) +- **Restart pipeline**: Stop and start the pipeline (required after publication changes) - **Edit destination**: Modify destination settings like credentials or advanced options - **Delete**: Remove the destination and permanently stop replication -### Disabling external replication (ETL) [#disabling-external-replication] +{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} -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)**. +### Disabling Pipelines + +To turn off Pipelines 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 Pipelines**. Disable external replication (ETL) from the Replication page actions menu -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). +For cleanup details, see [What happens when you disable Pipelines?](/docs/guides/database/replication/pipelines-faq#what-happens-when-you-disable-pipelines). ### Adding or removing tables @@ -272,7 +276,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 (ETL) 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 [Pipelines FAQ](/docs/guides/database/replication/pipelines-faq#what-happens-if-a-table-is-deleted-at-the-destination) for details. @@ -282,13 +286,13 @@ Schema change support depends on the destination. BigQuery is currently the only ### How it works -Once configured, external replication (ETL): +Once configured, a replication pipeline: 1. **Captures** changes from your Postgres database using Postgres publications and logical replication -2. **Streams** the changes through Supabase ETL +2. **Streams** the changes through the managed pipeline 3. **Loads** the data to your 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. +Pipelines automatically optimizes how changes are delivered to the destination. It currently performs data extraction and loading only, without transformation - your data is replicated as-is to the destination. ### Troubleshooting @@ -299,11 +303,11 @@ 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 (ETL) FAQ](/docs/guides/database/replication/external-replication-faq). +For more troubleshooting help, see the [Pipelines FAQ](/docs/guides/database/replication/pipelines-faq). ### Limitations -External replication (ETL) has the following limitations: +Pipelines has the following limitations: - **Primary keys required**: Tables must have primary keys (Postgres logical replication requirement) - **Custom data types**: Custom values replicate as strings. Check that your destination can interpret those string values correctly. @@ -311,12 +315,12 @@ External replication (ETL) has the following limitations: - **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 +- **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/pipelines-faq#can-data-duplicates-occur-during-pipeline-operations) for details Destination-specific limitations, such as BigQuery's row size limits, are documented in each destination guide. ### Next steps - [Set up BigQuery](/docs/guides/database/replication/bigquery) -- [Monitor external replication (ETL)](/docs/guides/database/replication/external-replication-monitoring) -- [View external replication (ETL) FAQ](/docs/guides/database/replication/external-replication-faq) +- [Monitor pipeline status](/docs/guides/database/replication/pipelines-monitoring) +- [View the Pipelines FAQ](/docs/guides/database/replication/pipelines-faq) diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index 4f284bc1dd5..96c0570f5dc 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 ( @@ -452,6 +452,12 @@ create table private.salaries ( ); ``` + + +If you want to access a custom schema through the Supabase Data API, you need to expose it and grant the appropriate permissions. See [Using Custom Schemas](/docs/guides/api/using-custom-schemas) for detailed steps. For security best practices around schema exposure, see [Securing your API](/docs/guides/api/securing-your-api). + + + ## Views A View is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user. @@ -579,7 +585,7 @@ from where courses.code != 'PG101'; ``` -Without a view, we would need to go into every dependent query to add the new rule. This would increase in the likelihood of errors and inconsistencies, as well as introducing a lot of effort for a developer. With views, we can alter just the underlying query in the view **transcripts**. The change will be applied to all applications using this view. +Without a view, we would need to go into every dependent query to add the new rule. This would increase in the likelihood of errors and inconsistencies, as well as introducing a lot of effort for a developer. With views, we can alter the underlying query in the view **transcripts**. The change will be applied to all applications using this view. #### Logical organization 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/database/vault.mdx b/apps/docs/content/guides/database/vault.mdx index 2a6f0788973..8d703a5d509 100644 --- a/apps/docs/content/guides/database/vault.mdx +++ b/apps/docs/content/guides/database/vault.mdx @@ -127,7 +127,7 @@ You should ensure that you protect access to this view with the appropriate SQL ### Updating secrets -A secret can be updated with the `vault.update_secret()` function, this function makes updating secrets easy, just provide the secret UUID as the first argument, and then an updated secret, updated optional unique name, or updated description: +To update a secret, use the `vault.update_secret()` function. Provide the secret UUID as the first argument, followed by an updated secret, name, or description: ```sql select diff --git a/apps/docs/content/guides/database/webhooks.mdx b/apps/docs/content/guides/database/webhooks.mdx index 3e6a27af497..47496c0c868 100644 --- a/apps/docs/content/guides/database/webhooks.mdx +++ b/apps/docs/content/guides/database/webhooks.mdx @@ -12,7 +12,7 @@ You can hook into three table events: `INSERT`, `UPDATE`, and `DELETE`. All even ## Webhooks vs triggers -Database Webhooks are very similar to triggers, and that's because Database Webhooks are just a convenience wrapper around triggers using the [pg_net](/docs/guides/database/extensions/pgnet) extension. This extension is asynchronous, and therefore will not block your database changes for long-running network requests. +Database Webhooks are very similar to triggers, and that's because Database Webhooks are a convenience wrapper around triggers using the [pg_net](/docs/guides/database/extensions/pgnet) extension. This extension is asynchronous, and therefore will not block your database changes for long-running network requests. This video demonstrates how you can create a new customer in Stripe each time a row is inserted into a `profiles` table: @@ -32,7 +32,7 @@ This video demonstrates how you can create a new customer in Stripe each time a 1. Select the table you want to hook into. 1. Select one or more events (table inserts, updates, or deletes) you want to hook into. -Since webhooks are just database triggers, you can also create one from SQL statement directly. +Since webhooks are database triggers, you can also create one from SQL statement directly. ```sql create trigger "my_webhook" after insert 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/database-migrations.mdx b/apps/docs/content/guides/deployment/database-migrations.mdx index cc2333d0cbe..d50618060c2 100644 --- a/apps/docs/content/guides/deployment/database-migrations.mdx +++ b/apps/docs/content/guides/deployment/database-migrations.mdx @@ -475,13 +475,13 @@ This creates a new migration file capturing the current remote schema. Commit it ### Step 3: If the migration history table is wrong -If a migration shows as missing in the remote history table but the schema change is actually already there (for example, it was applied manually), you can mark it as applied without re-running it: +If a migration shows as missing in the remote history table but the schema change is already there (for example, it was applied manually), you can mark it as applied without re-running it: ```bash name=Terminal supabase migration repair --status applied ``` -Or if a migration is recorded as applied but was never actually run: +Or if a migration is recorded as applied but was never run: ```bash name=Terminal supabase migration repair --status reverted diff --git a/apps/docs/content/guides/deployment/going-into-prod.mdx b/apps/docs/content/guides/deployment/going-into-prod.mdx index ce8879923f2..ffa3b028896 100644 --- a/apps/docs/content/guides/deployment/going-into-prod.mdx +++ b/apps/docs/content/guides/deployment/going-into-prod.mdx @@ -22,7 +22,7 @@ Check and review issues in your database using [Security Advisor](/dashboard/pro - Tables that do not have RLS enabled with reasonable policies allow any client to access and modify their data. This is usually not what you want. - [Learn more about RLS](/docs/guides/database/postgres/row-level-security). - Enable replication on tables containing sensitive data by enabling RLS and setting row security policies: - - Go to the [**Authentication > Policies**](/dashboard/project/_/auth/policies) section of the Supabase Dashboard to enable RLS and create security policies. + - Go to the [**Database > Policies**](/dashboard/project/_/database/policies) section of the Supabase Dashboard to enable RLS and create security policies. - Go to the [**Database > Publications**](/dashboard/project/_/database/publications) section of the Supabase Dashboard to manage replication tables. - Turn on [SSL Enforcement](/docs/guides/platform/ssl-enforcement) from the [**Database > Settings > SSL Configuration**](/dashboard/project/_/database/settings#ssl-configuration) section of the dashboard. - Enable [Network Restrictions](/docs/guides/platform/network-restrictions) for the database from the [**Database > Settings > Network Restrictions**](/dashboard/project/_/database/settings#network-restrictions) section of the dashboard. diff --git a/apps/docs/content/guides/deployment/shared-responsibility-model.mdx b/apps/docs/content/guides/deployment/shared-responsibility-model.mdx index adc1ec62ebd..e6d82cc9638 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 @@ -98,6 +98,7 @@ You can use Supabase to store and process Protected Health Information (PHI). Yo - Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-add-ons). - Turning on [SSL Enforcement](/docs/guides/platform/ssl-enforcement). - Enabling [Network Restrictions](/docs/guides/platform/network-restrictions). +- Keeping [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) enabled. Supabase sets `log_connections` to off by default for new projects. Projects that need HIPAA compliance should keep connection logging on for audit trails, and the Security Advisor warns if it is disabled. - Complying with encryption requirements in the HIPAA Security Rule. Data is encrypted at rest and in transit by Supabase. You can consider encrypting the data at your application layer. - Not storing PHI in [public Storage buckets](/docs/guides/storage/buckets/fundamentals#public-buckets). - Not [transferring projects](/docs/guides/platform/project-transfer) to a non-HIPAA organization. diff --git a/apps/docs/content/guides/functions.mdx b/apps/docs/content/guides/functions.mdx index 71839126532..2982243fa10 100644 --- a/apps/docs/content/guides/functions.mdx +++ b/apps/docs/content/guides/functions.mdx @@ -1,8 +1,8 @@ --- id: 'functions' title: 'Edge Functions' -description: 'Globally distributed TypeScript functions.' -subtitle: 'Globally distributed TypeScript functions.' +description: 'Run TypeScript functions globally at the edge.' +subtitle: 'Run TypeScript functions globally at the edge.' sidebar_label: 'Overview' tocVideo: 'za_loEtS4gs' --- @@ -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). @@ -41,295 +41,18 @@ Edge Functions are server-side TypeScript functions, distributed globally at the - Sending transactional emails. - Building messaging bots for Slack, Discord, etc. -
- -
+ ## Examples -Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository. +Check out [Supabase Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in GitHub or try these examples: -
-
- - - 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 b349cfe424f..816c715bfe6 100644 --- a/apps/docs/content/guides/functions/ai-models.mdx +++ b/apps/docs/content/guides/functions/ai-models.mdx @@ -358,7 +358,7 @@ 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' + import OpenAI from 'jsr:@openai/openai@^6' export default { fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => { 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 adde605ed48..607da02b5b0 100644 --- a/apps/docs/content/guides/functions/background-tasks.mdx +++ b/apps/docs/content/guides/functions/background-tasks.mdx @@ -9,7 +9,7 @@ Edge Function instances can process background tasks outside of the request hand This allows you to: -- Respond quickly to users while processing continues +- Respond to users while processing continues - Handle async operations without blocking the response --- @@ -27,7 +27,7 @@ EdgeRuntime.waitUntil(asyncLongRunningTask()) export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { - return new Response(...) + return Response.json({ ok: true }) }), } ``` @@ -42,7 +42,7 @@ export default { // Won't block the request, runs in background. EdgeRuntime.waitUntil(asyncLongRunningTask()) - return new Response(...) + return Response.json({ ok: true }) }), } ``` @@ -62,7 +62,7 @@ addEventListener('beforeunload', (ev) => { export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { - return new Response(...) + return Response.json({ ok: true }) }), } ``` diff --git a/apps/docs/content/guides/functions/connect-to-postgres.mdx b/apps/docs/content/guides/functions/connect-to-postgres.mdx index b3df92c5411..34869b62e5a 100644 --- a/apps/docs/content/guides/functions/connect-to-postgres.mdx +++ b/apps/docs/content/guides/functions/connect-to-postgres.mdx @@ -31,7 +31,7 @@ export default { return Response.json({ data }) } catch (err) { - return new Response(String(err?.message ?? err), { status: 500 }) + return Response.json({ error: String(err?.message ?? err) }, { status: 500 }) } }), } diff --git a/apps/docs/content/guides/functions/cors.mdx b/apps/docs/content/guides/functions/cors.mdx index 420aa5a74aa..eb0b2a1ed2f 100644 --- a/apps/docs/content/guides/functions/cors.mdx +++ b/apps/docs/content/guides/functions/cors.mdx @@ -31,10 +31,10 @@ If your function doesn't use `withSupabase`, add the headers yourself. See the [ -Import `corsHeaders` from `@supabase/supabase-js/cors` to automatically get all required headers: +Import `corsHeaders` from `npm:@supabase/supabase-js@^2/cors` to automatically get all required headers: ```ts index.ts -import { corsHeaders } from '@supabase/supabase-js/cors' +import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors' console.log(`Function "browser-with-cors" up and running!`) @@ -42,7 +42,7 @@ export default { fetch: async (req) => { // Handle the CORS preflight request. if (req.method === 'OPTIONS') { - return new Response('ok', { headers: corsHeaders }) + return Response.json({ ok: true }, { headers: corsHeaders }) } try { diff --git a/apps/docs/content/guides/functions/deploy.mdx b/apps/docs/content/guides/functions/deploy.mdx index e5ccb12e30d..d1a05e34bf1 100644 --- a/apps/docs/content/guides/functions/deploy.mdx +++ b/apps/docs/content/guides/functions/deploy.mdx @@ -40,7 +40,7 @@ If you haven't yet created a Supabase project, you can do so by visiting [databa -[Link](/docs/reference/cli/usage#supabase-link) your local project to your remote Supabase project using the ID you just retrieved: +[Link](/docs/reference/cli/usage#supabase-link) your local project to your remote Supabase project using the ID you retrieved: ```bash supabase link --project-ref your-project-id 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 04c258cb300..3ab5e0ab092 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,9 +46,9 @@ 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 { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime@^3' import { withSupabase } from 'npm:@supabase/server@^1' -import { decode } from 'npm:base64-arraybuffer' +import { decode } from 'npm:base64-arraybuffer@^1' console.log('Hello from Amazon Bedrock!') @@ -108,7 +108,7 @@ export default { upsert: false, }) if (!upload) { - return Response.json(uploadError) + return Response.json({ error: uploadError?.message ?? 'Upload failed' }, { status: 500 }) } const { data } = ctx.supabase.storage.from('images').getPublicUrl(upload.path!) return Response.json(data) 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 7b10c3140c6..459f7b7ae87 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,11 +34,11 @@ supabase functions new send-email Paste the following code into the `index.ts` file: ```tsx supabase/functions/send-email/index.ts -import { Webhook } from 'https://esm.sh/standardwebhooks@1.0.0' -import { renderAsync } from 'npm:@react-email/components@0.0.22' +import { Webhook } from 'npm:standardwebhooks@^1' +import { renderAsync } from 'npm:@react-email/components@^1' import { withSupabase } from 'npm:@supabase/server@^1' -import React from 'npm:react@18.3.1' -import { Resend } from 'npm:resend@4.0.0' +import React from 'npm:react@^19' +import { Resend } from 'npm:resend@^6' import { MagicLinkEmail } from './_templates/magic-link.tsx' @@ -48,7 +48,7 @@ const hookSecret = (Deno.env.get('SEND_EMAIL_HOOK_SECRET') as string).replace('v export default { fetch: withSupabase({ auth: 'none' }, async (req) => { if (req.method !== 'POST') { - return new Response('not allowed', { status: 400 }) + return Response.json({ error: 'not allowed' }, { status: 400 }) } const payload = await req.text() @@ -124,8 +124,8 @@ import { Link, Preview, Text, -} from 'npm:@react-email/components@0.0.22' -import * as React from 'npm:react@18.3.1' +} from 'npm:@react-email/components@^1' +import * as React from 'npm:react@^19' interface MagicLinkEmailProps { supabase_url: string diff --git a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx index f28412b1025..c845dc25ffe 100644 --- a/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx +++ b/apps/docs/content/guides/functions/examples/cloudflare-turnstile.mdx @@ -54,9 +54,9 @@ export default { const outcome = await result.json() console.log(outcome) if (outcome.success) { - return new Response('success') + return Response.json({ success: true }) } - return new Response('failure') + return Response.json({ success: false }) }), } ``` 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 d6ef95ec477..c013b25d8fb 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 @@ -102,8 +102,8 @@ In your newly created `supabase/functions/text-to-speech/index.ts` file, add the import 'jsr:@supabase/functions-js/edge-runtime.d.ts' import { withSupabase } from 'npm:@supabase/server@^1' -import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0' -import * as hash from 'npm:object-hash' +import { ElevenLabsClient } from 'npm:elevenlabs@^1' +import * as hash from 'npm:object-hash@^3' const client = new ElevenLabsClient({ apiKey: Deno.env.get('ELEVENLABS_API_KEY'), 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 e4913a30afa..8ec31003e50 100644 --- a/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx +++ b/apps/docs/content/guides/functions/examples/elevenlabs-transcribe-speech.mdx @@ -107,13 +107,13 @@ Since Supabase Edge Function uses the [Deno runtime](https://deno.land/), you do 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 { Bot, webhookCallback } from 'npm:grammy@^1' import 'jsr:@supabase/functions-js/edge-runtime.d.ts' import { withSupabase } from 'npm:@supabase/server@^1' -import type { SupabaseClient } from 'npm:@supabase/supabase-js@2' -import { ElevenLabsClient } from 'npm:elevenlabs@1.50.5' +import type { SupabaseClient } from 'npm:@supabase/supabase-js@^2' +import { ElevenLabsClient } from 'npm:elevenlabs@^1' console.log(`Function "elevenlabs-scribe-bot" up and running!`) @@ -235,7 +235,7 @@ export default { 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 Response.json({ error: 'not allowed' }, { status: 405 }) } supabaseAdmin = ctx.supabaseAdmin 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 71cef4ad1ee..4a19c24f374 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 { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts' -import React from 'https://esm.sh/react@18.2.0' +import { ImageResponse } from 'npm:@vercel/og@^0' +import React from 'npm:react@^19' export default function handler(req: Request) { return new ImageResponse( diff --git a/apps/docs/content/guides/functions/examples/push-notifications.mdx b/apps/docs/content/guides/functions/examples/push-notifications.mdx index b346ebe70b7..f6a80ac2aea 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) @@ -164,7 +164,7 @@ Push notifications are an important part of any mobile app. They allow you to se ```ts supabase/functions/push/index.ts import { withSupabase } from 'npm:@supabase/server@^1' - import { JWT } from 'npm:google-auth-library@9' + import { JWT } from 'npm:google-auth-library@^10' import serviceAccount from '../service-account.json' with { type: 'json' } interface Notification { diff --git a/apps/docs/content/guides/functions/examples/semantic-search.mdx b/apps/docs/content/guides/functions/examples/semantic-search.mdx index 47548c35d7d..e2a29dfa470 100644 --- a/apps/docs/content/guides/functions/examples/semantic-search.mdx +++ b/apps/docs/content/guides/functions/examples/semantic-search.mdx @@ -62,14 +62,14 @@ export default { .eq('id', id) if (error) console.warn(error.message) - return new Response('ok') + return Response.json({ ok: true }) }), } ``` ## 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): @@ -112,7 +112,7 @@ const model = new Supabase.ai.Session('gte-small') export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { const { search } = await req.json() - if (!search) return new Response('Please provide a search param!') + if (!search) return Response.json({ error: 'Please provide a search param!' }, { status: 400 }) // Generate embedding for search term. const embedding = await model.run(search, { mean_pool: true, @@ -128,7 +128,7 @@ export default { .select('content') .limit(3) if (error) { - return Response.json(error) + return Response.json({ error: error.message }, { status: 500 }) } return Response.json({ search, result }) @@ -136,4 +136,4 @@ export default { } ``` -You now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions! +You now have AI powered semantic search set up without any external dependencies! All you need: you, pgvector, and Supabase Edge Functions! diff --git a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx index 9a691e28233..1bdb92f7b96 100644 --- a/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx +++ b/apps/docs/content/guides/functions/examples/sentry-monitoring.mdx @@ -23,12 +23,12 @@ supabase functions new sentryfied Handle exceptions within your function and send them to Sentry. ```tsx -import * as Sentry from 'https://deno.land/x/sentry/index.mjs' +import * as Sentry from 'npm:@sentry/deno@^8' import { withSupabase } from 'npm:@supabase/server@^1' Sentry.init({ // https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn - dsn: SENTRY_DSN, + dsn: Deno.env.get('SENTRY_DSN'), defaultIntegrations: false, // Performance Monitoring tracesSampleRate: 1.0, @@ -55,7 +55,7 @@ export default { Sentry.captureException(e) // Flush Sentry before the running process closes await Sentry.flush(2000) - return Response.json({ msg: 'error' }, { status: 500 }) + return Response.json({ error: 'Internal Server Error' }, { status: 500 }) } }), } 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 9f78d84aedd..ea9007623b6 100644 --- a/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx +++ b/apps/docs/content/guides/functions/examples/slack-bot-mention.mdx @@ -20,14 +20,15 @@ For your bot to seamlessly interact with Slack, you'll need to configure Slack A Deploy the following code as an Edge function using the CLI: ```bash -supabase --project-ref nacho_slacker secrets \ -set SLACK_TOKEN= +supabase secrets set \ + SLACK_TOKEN= \ + --project-ref nacho_slacker ``` Here's the code of the Edge Function, you can change the response to handle the text received: ```ts index.ts -import { WebClient } from 'https://deno.land/x/slack_web_api@6.7.2/mod.js' +import { WebClient } from 'npm:@slack/web-api@^7' import { withSupabase } from 'npm:@supabase/server@^1' const slackBotToken = Deno.env.get('SLACK_TOKEN') ?? '' @@ -55,7 +56,7 @@ export default { text: `Hello <@${user}>!`, thread_ts: ts, }) - return new Response('ok', { status: 200 }) + return Response.json({ ok: true }) } } catch (error) { return Response.json({ error: error.message }, { 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 630de49aca5..7cd80c8ddaf 100644 --- a/apps/docs/content/guides/functions/examples/upstash-redis.mdx +++ b/apps/docs/content/guides/functions/examples/upstash-redis.mdx @@ -39,7 +39,7 @@ supabase functions new upstash-redis-counter 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 { Redis } from 'npm:@upstash/redis@^1' import { withSupabase } from 'npm:@supabase/server@^1' console.log(`Function "upstash-redis-counter" up and running!`) diff --git a/apps/docs/content/guides/functions/kysely-postgres.mdx b/apps/docs/content/guides/functions/kysely-postgres.mdx index 0d61b084322..9b98eb9b869 100644 --- a/apps/docs/content/guides/functions/kysely-postgres.mdx +++ b/apps/docs/content/guides/functions/kysely-postgres.mdx @@ -34,7 +34,7 @@ GET YOUR CERT FROM YOUR PROJECT DASHBOARD Create a `DenoPostgresDriver.ts` file to manage the connection to Postgres via [deno-postgres](https://deno-postgres.com/): ```ts DenoPostgresDriver.ts -import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts' +import { Pool, PoolClient } from 'jsr:@db/postgres@^0' import { CompiledQuery, DatabaseConnection, @@ -42,9 +42,9 @@ import { PostgresCursorConstructor, QueryResult, TransactionSettings, -} from 'https://esm.sh/kysely@0.23.4' -import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js' -import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js' +} from 'npm:kysely@^0' +import { freeze, isFunction } from 'npm:kysely@^0/dist/esm/util/object-utils.js' +import { extendStackTrace } from 'npm:kysely@^0/dist/esm/util/stack-trace-utils.js' export interface PostgresDialectConfig { pool: Pool | (() => Promise) @@ -190,14 +190,14 @@ class PostgresConnection implements DatabaseConnection { Create an `index.ts` file to execute a query on incoming requests: ```ts index.ts -import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts' +import { Pool } from 'jsr:@db/postgres@^0' import { Generated, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, -} from 'https://esm.sh/kysely@0.23.4' +} from 'npm:kysely@^0' import { withSupabase } from 'npm:@supabase/server@^1' import { PostgresDriver } from './DenoPostgresDriver.ts' @@ -258,23 +258,23 @@ export default { // 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 + const data = animals.map((animal) => + Object.fromEntries( + Object.entries(animal).map(([key, value]) => [ + key, + typeof value === 'bigint' ? value.toString() : value, + ]) + ) ) - // Return the response with the correct content type header - return new Response(body, { - status: 200, + return Response.json(data, { headers: { 'Content-Type': 'application/json; charset=utf-8', }, }) } catch (err) { console.error(err) - return new Response(String(err?.message ?? err), { status: 500 }) + return Response.json({ error: String(err?.message ?? err) }, { status: 500 }) } }), } diff --git a/apps/docs/content/guides/functions/recursive-functions.mdx b/apps/docs/content/guides/functions/recursive-functions.mdx index ad28b050c1e..d661924efd1 100644 --- a/apps/docs/content/guides/functions/recursive-functions.mdx +++ b/apps/docs/content/guides/functions/recursive-functions.mdx @@ -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 3e112ee2d20..872f5d708d6 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: { if (req.method === 'GET') { - return new Response('Hello World!') + return Response.json({ message: 'Hello World!' }) } const { name } = await req.json() if (name) { - return new Response(`Hello ${name}!`) + return Response.json({ message: `Hello ${name}!` }) } - return new Response('Hello World!') + return Response.json({ message: 'Hello World!' }) }), } ``` @@ -60,7 +60,7 @@ export default { ```ts -import express from 'npm:express@4.18.2' +import express from 'npm:express@^5' const app = express() app.use(express.json()) @@ -70,12 +70,12 @@ app.use(express.json()) const port = 3000 app.get('/hello-world', (req, res) => { - res.send('Hello World!') + res.json({ message: 'Hello World!' }) }) app.post('/hello-world', (req, res) => { const { name } = req.body - res.send(`Hello ${name}!`) + res.json({ message: `Hello ${name}!` }) }) app.listen(port, () => { @@ -88,18 +88,18 @@ app.listen(port, () => { ```ts -import { Application } from 'jsr:@oak/oak@15/application' -import { Router } from 'jsr:@oak/oak@15/router' +import { Application } from 'jsr:@oak/oak@^17/application' +import { Router } from 'jsr:@oak/oak@^17/router' const router = new Router() router.get('/hello-world', (ctx) => { - ctx.response.body = 'Hello world!' + ctx.response.body = { message: 'Hello World!' } }) router.post('/hello-world', async (ctx) => { const { name } = await ctx.request.body.json() - ctx.response.body = `Hello ${name}!` + ctx.response.body = { message: `Hello ${name}!` } }) const app = new Application() @@ -114,17 +114,17 @@ app.listen({ port: 3000 }) ```ts -import { Hono } from 'jsr:@hono/hono' +import { Hono } from 'jsr:@hono/hono@^4' const app = new Hono() app.post('/hello-world', async (c) => { const { name } = await c.req.json() - return new Response(`Hello ${name}!`) + return c.json({ message: `Hello ${name}!` }) }) app.get('/hello-world', (c) => { - return new Response('Hello World!') + return c.json({ message: 'Hello World!' }) }) export default { fetch: app.fetch } @@ -173,15 +173,15 @@ let tasks: Task[] = [] const router = new Map Promise>() async function getAllTasks(): Promise { - return new Response(JSON.stringify(tasks)) + return Response.json({ tasks }) } async function getTask(id: string): Promise { const task = tasks.find((t) => t.id === id) if (task) { - return new Response(JSON.stringify(task)) + return Response.json({ task }) } else { - return new Response('Task not found', { status: 404 }) + return Response.json({ error: 'Task not found' }, { status: 404 }) } } @@ -189,16 +189,17 @@ async function createTask(req: Request): Promise { const id = Math.random().toString(36).substring(7) const task = { id, name: '' } tasks.push(task) - return new Response(JSON.stringify(task), { status: 201 }) + return Response.json({ task }, { status: 201 }) } async function updateTask(id: string, req: Request): Promise { const index = tasks.findIndex((t) => t.id === id) if (index !== -1) { - tasks[index] = { ...tasks[index] } - return new Response(JSON.stringify(tasks[index])) + const updates = await req.json() + tasks[index] = { ...tasks[index], ...updates } + return Response.json({ task: tasks[index] }) } else { - return new Response('Task not found', { status: 404 }) + return Response.json({ error: 'Task not found' }, { status: 404 }) } } @@ -206,9 +207,9 @@ async function deleteTask(id: string): Promise { const index = tasks.findIndex((t) => t.id === id) if (index !== -1) { tasks.splice(index, 1) - return new Response('Task deleted successfully') + return Response.json({ message: 'Task deleted successfully' }) } else { - return new Response('Task not found', { status: 404 }) + return Response.json({ error: 'Task not found' }, { status: 404 }) } } @@ -234,19 +235,19 @@ export default { if (id) { return updateTask(id, req) } else { - return new Response('Bad Request', { status: 400 }) + return Response.json({ error: 'Bad Request' }, { status: 400 }) } case 'DELETE': if (id) { return deleteTask(id) } else { - return new Response('Bad Request', { status: 400 }) + return Response.json({ error: 'Bad Request' }, { status: 400 }) } default: - return new Response('Method Not Allowed', { status: 405 }) + return Response.json({ error: 'Method Not Allowed' }, { status: 405 }) } } catch (error) { - return new Response(`Internal Server Error: ${error}`, { status: 500 }) + return Response.json({ error: `Internal Server Error: ${error}` }, { status: 500 }) } }), } @@ -257,7 +258,7 @@ export default { ```ts -import express from 'npm:express@4.18.2' +import express from 'npm:express@^5' const app = express() app.use(express.json()) @@ -293,8 +294,8 @@ app.delete('/tasks/:id', async (req, res) => { ```ts -import { Application } from 'jsr:@oak/oak/application' -import { Router } from 'jsr:@oak/oak/router' +import { Application } from 'jsr:@oak/oak@^17/application' +import { Router } from 'jsr:@oak/oak@^17/router' const router = new Router() @@ -357,7 +358,7 @@ app.listen({ port: 3000 }) ```ts -import { Hono } from 'jsr:@hono/hono' +import { Hono } from 'jsr:@hono/hono@^4' // You can set the basePath with Hono const functionName = 'tasks' @@ -368,9 +369,9 @@ app.get('/:id', async (c) => { const id = c.req.param('id') const task = {} // Fetch task by id here if (task) { - return new Response(JSON.stringify(task)) + return c.json({ task }) } else { - return new Response('Task not found', { status: 404 }) + return c.json({ error: 'Task not found' }, { status: 404 }) } }) @@ -381,9 +382,9 @@ app.patch('/:id', async (c) => { const task = {} // Fetch task by id here if (task) { Object.assign(task, updates) - return new Response(JSON.stringify(task)) + return c.json({ task }) } else { - return new Response('Task not found', { status: 404 }) + return c.json({ error: 'Task not found' }, { status: 404 }) } }) @@ -392,9 +393,9 @@ app.delete('/:id', async (c) => { const task = {} // Fetch task by id here if (task) { // Delete task - return new Response('Task deleted successfully') + return c.json({ message: 'Task deleted successfully' }) } else { - return new Response('Task not found', { status: 404 }) + return c.json({ error: 'Task not found' }, { status: 404 }) } }) 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 8129b4e2af6..d6a47758e65 100644 --- a/apps/docs/content/guides/functions/websockets.mdx +++ b/apps/docs/content/guides/functions/websockets.mdx @@ -5,7 +5,8 @@ description: 'How to handle WebSocket connections in Edge Functions' subtitle: 'Handle WebSocket connections in Edge Functions.' --- -Edge Functions supports hosting WebSocket servers that can facilitate bi-directional communications with browser clients. +Edge Functions supports hosting WebSocket servers that can facilitate bi-directional +communications with browser clients. This allows you to: @@ -13,7 +14,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). +For a production-ready reconnect pattern with session persistence and replay, see +[Resumable WebSockets with Edge Functions](/docs/guides/functions/examples/resumable-websockets). --- @@ -36,7 +38,10 @@ export default { const upgrade = req.headers.get('upgrade') || '' if (upgrade.toLowerCase() != 'websocket') { - return new Response("request isn't trying to upgrade to WebSocket.", { status: 400 }) + return Response.json( + { error: "request isn't trying to upgrade to WebSocket." }, + { status: 400 } + ) } const { socket, response } = Deno.upgradeWebSocket(req) @@ -61,7 +66,7 @@ export default { ```ts import { createServer } from 'node:http' -import { WebSocketServer } from 'npm:ws' +import { WebSocketServer } from 'npm:ws@^8' const server = createServer() // Since we manually created the HTTP server, @@ -105,7 +110,9 @@ server.listen(8080) You can also establish an outbound WebSocket connection to another server from an Edge Function. -Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a WebSocket proxy, for example as a [relay server](https://github.com/supabase-community/openai-realtime-console?tab=readme-ov-file#using-supabase-edge-functions-as-a-relay-server) for the [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview). +Combining it with incoming WebSocket servers, it's possible to use Edge Functions as a +WebSocket proxy, for example as a [relay server](https://github.com/supabase-community/openai-realtime-console?tab=readme-ov-file#using-supabase-edge-functions-as-a-relay-server) +for the [OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/overview). <$CodeSample external={true} @@ -121,11 +128,17 @@ lines={[[1, 3], [5, -1]]} ## Authentication -WebSocket browser clients don't have the option to send custom headers. Because of this, Edge Functions won't be able to perform the usual authorization header check to verify the JWT. +WebSocket browser clients don't have the option to send custom headers. Because of this, +Edge Functions won't be able to perform the usual authorization header check to verify +the JWT. -You can skip the default authorization header checks by explicitly providing `--no-verify-jwt` when serving and deploying functions. +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. 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. +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. ```ts -import { createClient } from 'npm:@supabase/supabase-js@2' +import { createClient } from 'npm:@supabase/supabase-js@^2' const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) const supabase = createClient( @@ -150,7 +163,10 @@ 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 }) + return Response.json( + { error: "request isn't trying to upgrade to WebSocket." }, + { status: 400 } + ) } // Please be aware query params may be logged in some logging systems. @@ -159,19 +175,19 @@ export default { if (!jwt) { console.error('Auth token not provided') - return new Response('Auth token not provided', { status: 403 }) + return Response.json({ error: 'Auth token not provided' }, { status: 403 }) } const { error, data } = await supabase.auth.getUser(jwt) if (error) { console.error(error) - return new Response('Invalid token provided', { status: 403 }) + return Response.json({ error: 'Invalid token provided' }, { status: 403 }) } if (!data.user) { console.error('user is not authenticated') - return new Response('User is not authenticated', { status: 403 }) + return Response.json({ error: 'User is not authenticated' }, { status: 403 }) } const { socket, response } = Deno.upgradeWebSocket(req) @@ -194,7 +210,7 @@ export default { ```ts -import { createClient } from 'npm:@supabase/supabase-js@2' +import { createClient } from 'npm:@supabase/supabase-js@^2' const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!) const supabase = createClient( @@ -207,7 +223,10 @@ 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 }) + return Response.json( + { error: "request isn't trying to upgrade to WebSocket." }, + { status: 400 } + ) } // Sec-WebScoket-Protocol may return multiple protocol values `jwt-TOKEN, value1, value 2` @@ -219,18 +238,18 @@ export default { if (!jwt) { console.error('Auth token not provided') - return new Response('Auth token not provided', { status: 403 }) + return Response.json({ error: 'Auth token not provided' }, { status: 403 }) } const { error, data } = await supabase.auth.getUser(jwt) if (error) { console.error(error) - return new Response('Invalid token provided', { status: 403 }) + return Response.json({ error: 'Invalid token provided' }, { status: 403 }) } if (!data.user) { console.error('user is not authenticated') - return new Response('User is not authenticated', { status: 403 }) + return Response.json({ error: 'User is not authenticated' }, { status: 403 }) } const { socket, response } = Deno.upgradeWebSocket(req) @@ -254,17 +273,24 @@ export default { -The maximum duration is capped based on the wall-clock, CPU, and memory limits. The Function will shutdown when it reaches one of these [limits](/docs/guides/functions/limits). +The maximum duration is capped based on the wall-clock, CPU, and memory limits. The +Function will shutdown when it reaches one of these +[limits](/docs/guides/functions/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`. +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 -When testing Edge Functions locally with Supabase CLI, the instances are terminated automatically after a request is completed. This will prevent keeping WebSocket connections open. +When testing Edge Functions locally with Supabase CLI, the instances are terminated +automatically after a request is completed. This will prevent keeping WebSocket +connections open. To prevent that, you can update the `supabase/config.toml` with the following settings: @@ -275,6 +301,7 @@ policy = "per_worker" -When running with `per_worker` policy, Function won't auto-reload on edits. You will need to manually restart it by running `supabase functions serve`. +When running with `per_worker` policy, Function won't auto-reload on edits. You will +need to manually restart it by running `supabase functions serve`. diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index 45a0a7892cc..f0ff033351a 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -5,36 +5,7 @@ description: 'Resources for getting started with Supabase.' hideToc: true --- -
- -
- -
- - - Develop with Supabase AI-first using plugins, MCP, and skills. - - - - - Learn about the different API keys in Supabase and how to use them. - - - - - Use the Supabase CLI to develop locally and collaborate between teams. - - -
- -
- -
+ ### Use cases diff --git a/apps/docs/content/guides/getting-started/api-keys.mdx b/apps/docs/content/guides/getting-started/api-keys.mdx index 696c9578bbe..b1cff12b380 100644 --- a/apps/docs/content/guides/getting-started/api-keys.mdx +++ b/apps/docs/content/guides/getting-started/api-keys.mdx @@ -32,6 +32,12 @@ 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. | + + +Both key types work simultaneously. Creating publishable and secret keys adds them _alongside_ your existing `anon` and `service_role` keys without affecting them — your legacy keys keep working. They remain valid until you explicitly disable them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard which is a separate step. See [Migrating to new API keys](/docs/guides/getting-started/migrating-to-new-api-keys) for the full process. + + + <$Partial path="api_keys_deprecation.mdx" /> ## Publishable keys @@ -89,7 +95,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 8dcb43769e0..285be68ead4 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 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. +Automatically replicate your database to destination systems like data warehouses and analytics platforms with Supabase Pipelines. Read [the documentation](/docs/guides/database/replication/pipelines) 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/quickstarts/astrojs.mdx b/apps/docs/content/guides/getting-started/quickstarts/astrojs.mdx index 1dd467b0fb3..392227c9fb6 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/astrojs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/astrojs.mdx @@ -2,191 +2,124 @@ title: 'Use Supabase with Astro' subtitle: 'Learn how to create a Supabase project, add sample data, and query from an Astro app.' breadcrumb: 'Framework Quickstarts' -hideToc: false --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create an Astro app - <$Partial path="quickstart_db_setup.mdx" /> +Create an Astro app using the `npm create` command. - +```bash +npm create astro@latest my-app +cd my-app +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - Create an Astro app using the `npm create` command. +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install Supabase client library and Node adapter - +Install the `supabase-js` client library and the `@astrojs/node` adapter to enable server-side rendering. - ```bash name=Terminal - npm create astro@latest my-app - cd my-app - ``` +```bash +npm install @supabase/supabase-js @astrojs/node +``` - +## 6. Configure Astro for SSR - +Update your `astro.config.mjs`. - - +```js name=astro.config.mjs +import node from '@astrojs/node' +import { defineConfig } from 'astro/config' - Install the `supabase-js` client library and the `@astrojs/node` adapter to enable server-side rendering. +export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone', + }), +}) +``` - +## 7. Declare Supabase environment variables - +Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=astro&tab=frameworks): - ```bash name=Terminal - npm install @supabase/supabase-js @astrojs/node - ``` + - +```text name=.env.local +PUBLIC_SUPABASE_URL= +PUBLIC_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} /> - - +## 8. Create a Supabase client helper - Update your `astro.config.mjs`. +Create a utility file to initialize the Supabase client: - +```ts name=src/lib/supabase.ts +import { createClient } from '@supabase/supabase-js' - +const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL +const supabasePublishableKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY - ```js name=astro.config.mjs - import { defineConfig } from "astro/config"; - import node from "@astrojs/node"; +export function createServerClient() { + return createClient(supabaseUrl, supabasePublishableKey) +} +``` - export default defineConfig({ - output: "server", - adapter: node({ - mode: "standalone", - }), - }); - ``` +## 9. Query Supabase data from Astro - +Create a new file at `src/pages/instruments.astro` and populate with the following. - +This queries all rows from the `instruments` table you created earlier and renders them on the page. - - +```astro name=src/pages/instruments.astro +--- +import { createServerClient } from "../lib/supabase"; - Create a `.env.local` file and populate with your Supabase connection variables: +const supabase = createServerClient(); +const { data: instruments } = await supabase.from("instruments").select(); +--- - - + + + Instruments + + +
    + {instruments?.map((instrument) => ( +
  • {instrument.name}
  • + ))} +
+ + +``` -
+## 10. Start the app - +Run the development server, go to http://localhost:4321/instruments in your browser of choice to check the list of instruments. - <$CodeTabs> - - ```text name=.env.local - PUBLIC_SUPABASE_URL= - PUBLIC_SUPABASE_PUBLISHABLE_KEY= - ``` - - - - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} /> - - - -
- - - - - Create a utility file to initialize the Supabase client: - - - - - - ```ts name=src/lib/supabase.ts - import { createClient } from "@supabase/supabase-js"; - - const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL - const supabasePublishableKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY - - export function createServerClient() { - return createClient( - supabaseUrl, - supabasePublishableKey - ); - } - ``` - - - - - - - - - Create a new file at `src/pages/instruments.astro` and populate with the following. - - This queries all rows from the `instruments` table in Supabase and renders them on the page. - - - - - - ```astro name=src/pages/instruments.astro - --- - import { createServerClient } from "../lib/supabase"; - - const supabase = createServerClient(); - const { data: instruments } = await supabase.from("instruments").select(); - --- - - - - Instruments - - -
    - {instruments?.map((instrument) => ( -
  • {instrument.name}
  • - ))} -
- - - ``` - -
- -
- - - - - Run the development server, go to http://localhost:4321/instruments in your browser of choice to check the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - -
+```bash +npm run dev +``` ## Next steps +- Explore [drop-in UI components](/ui) for your Supabase app - Set up [Auth](/docs/guides/auth) for your app - [Insert more data](/docs/guides/database/import-data) into your database - Upload and serve static files using [Storage](/docs/guides/storage) diff --git a/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx b/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx index a4f1b58e0a8..d9b581b5e9f 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/expo-react-native.mdx @@ -2,191 +2,138 @@ title: 'Use Supabase with Expo React Native' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an Expo app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create an Expo app - <$Partial path="quickstart_db_setup.mdx" /> +Create a minimal Expo app using the `create-expo-app` command with the blank TypeScript template. - +```bash +npx create-expo-app my-app --template blank-typescript +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - Create a minimal Expo app using the `create-expo-app` command with the blank TypeScript template. +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - +The fastest way to get started is to use the `@supabase/supabase-js` client library which provides a convenient interface for working with Supabase from a React Native app. - ```bash name=Terminal - npx create-expo-app my-app --template blank-typescript - ``` +Navigate to the Expo app and install `supabase-js` along with the required dependencies for session storage and URL handling. - +```bash +cd my-app && npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite +``` - +## 6. Declare Supabase environment variables - - +Create a `.env` file in the root of your project and populate it with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true). - The fastest way to get started is to use the `@supabase/supabase-js` client library which provides a convenient interface for working with Supabase from a React Native app. + - Navigate to the Expo app and install `supabase-js` along with the required dependencies for secure storage and URL handling. +Expo requires environment variables to be prefixed with `EXPO_PUBLIC_` to be accessible in your app code. - +```text name=.env +EXPO_PUBLIC_SUPABASE_URL= +EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} /> - ```bash name=Terminal - cd my-app && npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite - ``` +## 7. Initialize the Supabase client - +Create a helper file at `lib/supabase.ts` to initialize the Supabase client using the environment variables. - +The code below uses Expo's localStorage polyfill to persist authentication sessions. - - +```ts name=lib/supabase.ts +import 'react-native-url-polyfill/auto' - Create a `.env` file in the root of your project and populate it with your Supabase connection variables. +import { createClient } from '@supabase/supabase-js' - Expo requires environment variables to be prefixed with `EXPO_PUBLIC_` to be accessible in your app code. +import 'expo-sqlite/localStorage/install' - - +const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL +const supabasePublishableKey = process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY +export const supabase = createClient(supabaseUrl, supabasePublishableKey, { + auth: { + storage: localStorage, + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + }, +}) +``` - +## 8. Query data from the app - +Replace the contents of `App.tsx` with the following code to fetch and display the instruments from your database. - ```text name=.env - EXPO_PUBLIC_SUPABASE_URL= - EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY= - ``` - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "", "tab": "" }} /> +Use `useEffect` to fetch the data when the component mounts and display the query result using React Native components. - +```tsx name=App.tsx +import { useEffect, useState } from 'react' +import { FlatList, StyleSheet, Text, View } from 'react-native' - +import { supabase } from './lib/supabase' - - +export default function App() { + const [instruments, setInstruments] = useState([]) - Create a helper file at `lib/supabase.ts` to initialize the Supabase client using the environment variables. + useEffect(() => { + getInstruments() + }, []) - The code below uses Expo's localStorage polyfill to persist authentication sessions. + async function getInstruments() { + const { data } = await supabase.from('instruments').select() + setInstruments(data) + } - + return ( + + item.id.toString()} + renderItem={({ item }) => {item.name}} + /> + + ) +} - +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + paddingTop: 50, + paddingHorizontal: 16, + }, + item: { + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#ccc', + }, +}) +``` - ```ts name=lib/supabase.ts - import 'react-native-url-polyfill/auto' - import { createClient } from '@supabase/supabase-js' - import 'expo-sqlite/localStorage/install'; +## 9. Start the app - const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL - const supabasePublishableKey = process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY +Run the development server and scan the QR code with the Expo Go app on your phone, or press `i` for iOS simulator or `a` for Android emulator. - export const supabase = createClient(supabaseUrl, supabasePublishableKey, { - auth: { - storage: localStorage, - autoRefreshToken: true, - persistSession: true, - detectSessionInUrl: false, - }, - }) - ``` - - - - - - - - - Replace the contents of `App.tsx` with the following code to fetch and display the instruments from your database. - - Use `useEffect` to fetch the data when the component mounts and display the query result using React Native components. - - - - - ```tsx name=App.tsx - import { useEffect, useState } from 'react' - import { StyleSheet, View, FlatList, Text } from 'react-native' - import { supabase } from './lib/supabase' - - export default function App() { - const [instruments, setInstruments] = useState([]) - - useEffect(() => { - getInstruments() - }, []) - - async function getInstruments() { - const { data } = await supabase.from('instruments').select() - setInstruments(data) - } - - return ( - - item.id.toString()} - renderItem={({ item }) => ( - {item.name} - )} - /> - - ) - } - - const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - paddingTop: 50, - paddingHorizontal: 16, - }, - item: { - padding: 16, - borderBottomWidth: 1, - borderBottomColor: '#ccc', - }, - }) - ``` - - - - - - - - - Run the development server and scan the QR code with the Expo Go app on your phone, or press `i` for iOS simulator or `a` for Android emulator. - - - - - - ```bash name=Terminal - npx expo start - ``` - - - - - - +```bash +npx expo start +``` ## Next steps diff --git a/apps/docs/content/guides/getting-started/quickstarts/flask.mdx b/apps/docs/content/guides/getting-started/quickstarts/flask.mdx index c20e845f9ea..aa3be961447 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/flask.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/flask.mdx @@ -2,150 +2,103 @@ title: 'Use Supabase with Python' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Python app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Python app with Flask - <$Partial path="quickstart_db_setup.mdx" /> +Create a new directory for your Python app and set up a virtual environment. - +```bash +mkdir my-app && cd my-app +python3 -m venv venv +source venv/bin/activate +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - Create a new directory for your Python app and set up a virtual environment. +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install Flask and the Supabase client library - ```bash name=Terminal - mkdir my-app && cd my-app - python3 -m venv venv - source venv/bin/activate - ``` +The fastest way to get started is to use Flask for the web framework and the `supabase-py` client library which provides a convenient interface for working with Supabase from a Python app. - +Install both packages using pip. - +```bash +pip install flask supabase +``` - - +## 6. Create environment variables file - The fastest way to get started is to use Flask for the web framework and the `supabase-py` client library which provides a convenient interface for working with Supabase from a Python app. +Create a `.env` file in your project root and populate it with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true): - Install both packages using pip. + - +```text name=.env +SUPABASE_URL= +SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} /> - ```bash name=Terminal - pip install flask supabase - ``` +## 7. Query data from the app - +Install the `python-dotenv` package to load environment variables: - +```bash +pip install python-dotenv +``` - - +Create an `app.py` file and add a route that fetches data from your `instruments` table using the Supabase client. - Create a `.env` file in your project root and populate it with your Supabase connection variables: +```python name=app.py +import os +from flask import Flask +from supabase import create_client, Client +from dotenv import load_dotenv - - +load_dotenv() +app = Flask(__name__) - +supabase: Client = create_client( + os.environ.get("SUPABASE_URL"), + os.environ.get("SUPABASE_PUBLISHABLE_KEY") +) - +@app.route('/') +def index(): + response = supabase.table('instruments').select("*").execute() + instruments = response.data - <$CodeTabs> + html = '

Instruments

    ' + for instrument in instruments: + html += f'
  • {instrument["name"]}
  • ' + html += '
' - ```text name=.env - SUPABASE_URL= - SUPABASE_PUBLISHABLE_KEY= - ``` + return html - +if __name__ == '__main__': + app.run(debug=True) +``` - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "", "tab": "" }} /> +## 8. Start the app -
+Run the Flask development server, and go to http://localhost:5000 in your browser, you should see the list of instruments. -
- - - - - Install the `python-dotenv` package to load environment variables: - - ```bash - pip install python-dotenv - ``` - - Create an `app.py` file and add a route that fetches data from your `instruments` table using the Supabase client. - - - - - ```python name=app.py - import os - from flask import Flask - from supabase import create_client, Client - from dotenv import load_dotenv - - load_dotenv() - - app = Flask(__name__) - - supabase: Client = create_client( - os.environ.get("SUPABASE_URL"), - os.environ.get("SUPABASE_PUBLISHABLE_KEY") - ) - - @app.route('/') - def index(): - response = supabase.table('instruments').select("*").execute() - instruments = response.data - - html = '

Instruments

    ' - for instrument in instruments: - html += f'
  • {instrument["name"]}
  • ' - html += '
' - - return html - - if __name__ == '__main__': - app.run(debug=True) - ``` - -
- -
- - - - - Run the Flask development server, go to http://localhost:5000 in a browser and you should see the list of instruments. - - - - - - ```bash name=Terminal - python app.py - ``` - - - - -
+```bash +python app.py +``` ## Next steps diff --git a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx index 498936ca67f..c34cef02b6a 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx @@ -2,179 +2,133 @@ title: 'Use Supabase with Flutter' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Flutter app - <$Partial path="quickstart_db_setup.mdx" /> +Create a Flutter app using the `flutter create` command. - +```bash +flutter create my_app +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - Create a Flutter app using the `flutter create` command. You can skip this step if you already have a working app. +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - ```bash name=Terminal - flutter create my_app - ``` +The fastest way to get started is to use the [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) client library which provides a convenient interface for working with Supabase from a Flutter app. - +Open the `pubspec.yaml` file inside your Flutter app and add `supabase_flutter` as a dependency. - +```yaml name=pubspec.yaml +supabase_flutter: ^2.0.0 +``` - +## 6. Initialize the Supabase client - +Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=flutter&tab=mobiles): - The fastest way to get started is to use the [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) client library which provides a convenient interface for working with Supabase from a Flutter app. + - Open the `pubspec.yaml` file inside your Flutter app and add `supabase_flutter` as a dependency. +```dart name=lib/main.dart +import 'package:supabase_flutter/supabase_flutter.dart'; - +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); - + await Supabase.initialize( + url: 'YOUR_SUPABASE_URL', + publishableKey: 'YOUR_SUPABASE_PUBLISHABLE_KEY', + ); + runApp(MyApp()); +} +``` - ```yaml name=pubspec.yaml - supabase_flutter: ^2.0.0 - ``` +<$Partial path="api_settings.mdx" variables={{ "framework": "flutter", "tab": "mobiles" }} /> - +## 7. Query data from the app - +Use a `FutureBuilder` to fetch the data when the home page loads and display the query result in a `ListView`. - +Replace the default `MyApp` and `MyHomePage` classes with the following code. - +```dart name=lib/main.dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); - Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and publishable key: + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Instruments', + home: HomePage(), + ); + } +} - - +class HomePage extends StatefulWidget { + const HomePage({super.key}); + @override + State createState() => _HomePageState(); +} - +class _HomePageState extends State { + final _future = Supabase.instance.client + .from('instruments') + .select(); - - - ```dart name=lib/main.dart - import 'package:supabase_flutter/supabase_flutter.dart'; - - Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - - await Supabase.initialize( - url: 'YOUR_SUPABASE_URL', - publishableKey: 'YOUR_SUPABASE_PUBLISHABLE_KEY', - ); - runApp(MyApp()); - } - ``` - - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "flutter", "tab": "mobiles" }} /> - - - - - - - - - - Use a `FutureBuilder` to fetch the data when the home page loads and display the query result in a `ListView`. - - Replace the default `MyApp` and `MyHomePage` classes with the following code. - - - - - - ```dart name=lib/main.dart - class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Instruments', - home: HomePage(), + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + final instruments = snapshot.data!; + return ListView.builder( + itemCount: instruments.length, + itemBuilder: ((context, index) { + final instrument = instruments[index]; + return ListTile( + title: Text(instrument['name']), + ); + }), ); - } - } + }, + ), + ); + } +} +``` - class HomePage extends StatefulWidget { - const HomePage({super.key}); +## 8. Start the app - @override - State createState() => _HomePageState(); - } +Run your app on a platform of your choosing! By default an app should launch in your web browser. - class _HomePageState extends State { - final _future = Supabase.instance.client - .from('instruments') - .select(); +Note that `supabase_flutter` is compatible with web, iOS, Android, macOS, and Windows apps. +Running the app on macOS requires additional configuration to [set the entitlements](https://docs.flutter.dev/development/platform-integration/macos/building#setting-up-entitlements). - @override - Widget build(BuildContext context) { - return Scaffold( - body: FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } - final instruments = snapshot.data!; - return ListView.builder( - itemCount: instruments.length, - itemBuilder: ((context, index) { - final instrument = instruments[index]; - return ListTile( - title: Text(instrument['name']), - ); - }), - ); - }, - ), - ); - } - } - ``` +```bash +flutter run +``` - - - - - - - - Run your app on a platform of your choosing! By default an app should launch in your web browser. - - Note that `supabase_flutter` is compatible with web, iOS, Android, macOS, and Windows apps. - Running the app on macOS requires additional configuration to [set the entitlements](https://docs.flutter.dev/development/platform-integration/macos/building#setting-up-entitlements). - - - - - - ```bash name=Terminal - flutter run - ``` - - - - - - - -## Setup deep links +## 9. Setup deep links (optional) Many sign in methods require deep links to redirect the user back to your app after authentication. Read more about setting deep links up for all platforms (including web) in the [Flutter Mobile Guide](/docs/guides/getting-started/tutorials/with-flutter#setup-deep-links). diff --git a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx index 4f81cce2f7f..6d9c331172c 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx @@ -2,91 +2,58 @@ title: 'Use Supabase with Hono' subtitle: '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.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Hono app - +Bootstrap the Hono example app from the Supabase Samples using the CLI. - Bootstrap the Hono example app from the Supabase Samples using the CLI. +```bash +npx supabase@latest bootstrap hono +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - ```bash name=Terminal - npx supabase@latest bootstrap hono - ``` +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - - +The `package.json` file in the project includes the necessary dependencies, including `@supabase/supabase-js` and `@supabase/ssr` to help with server-side auth. - The `package.json` file in the project includes the necessary dependencies, including `@supabase/supabase-js` and `@supabase/ssr` to help with server-side auth. +```bash +npm install +``` - +## 6. Set up the required environment variables - +Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&tab=frameworks). - ```bash name=Terminal - npm install - ``` +Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings. - - - - - - - - Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and publishable key. - - Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings. - - - - - - - - - - ```bash name=Terminal - cp .env.example .env - ``` +```bash +cp .env.example .env +``` {/* TODO: Not ideal for frameworks that have no entry in Connect */} -<$Partial path="api_settings_steps.mdx" variables={{ "framework": "", "tab": "frameworks" }} /> +<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "frameworks" }} /> - +## 7. Start the app - +Start the app, go to http://localhost:5173. - - +Learn how [server side auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=hono) works with Hono. - Start the app, go to http://localhost:5173. - - Learn how [server side auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=hono) works with Hono. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - - +```bash +npm run dev +``` ## Next steps diff --git a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx index d19a1ddb758..24523e7027e 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx @@ -2,148 +2,104 @@ title: 'Use Supabase with iOS and SwiftUI' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create an iOS SwiftUI app with Xcode - <$Partial path="quickstart_db_setup.mdx" /> +Select the **Xcode > New Project > iOS > App** menu item. - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - +To install, run the following command in the root of your project: - Open Xcode > New Project > iOS > App. You can skip this step if you already have a working app. +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - +Add the [supabase-swift](https://github.com/supabase/supabase-swift) package to your app using the Swift Package Manager. - +In Xcode, navigate to **File > Add Package Dependencies...** and enter the repository URL `https://github.com/supabase/supabase-swift` in the search bar. For detailed instructions, see Apple's [tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). - +Make sure to add `Supabase` product package as a dependency to your application target. - Add the [supabase-swift](https://github.com/supabase/supabase-swift) package to your app using the Swift Package Manager. +## 6. Initialize the Supabase client - In Xcode, navigate to **File > Add Package Dependencies...** and enter the repository URL `https://github.com/supabase/supabase-swift` in the search bar. For detailed instructions, see Apple's [tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app). +Create a new `Supabase.swift` file add a new Supabase instance using your project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=swift&tab=mobiles): - Make sure to add `Supabase` product package as a dependency to your application target. + - +```swift name=Supabase.swift +import Supabase - +let supabase = SupabaseClient( + supabaseURL: URL(string: "YOUR_SUPABASE_URL")!, + supabaseKey: "YOUR_SUPABASE_PUBLISHABLE_KEY" +) +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} /> - +## 7. Create a data model for instruments - Create a new `Supabase.swift` file add a new Supabase instance using your project URL and publishable key: +Create a decodable struct to deserialize the data from the database. - - +Add the following code to a new file named `Instrument.swift`. +```swift name=Instrument.swift +struct Instrument: Decodable, Identifiable { + let id: Int + let name: String +} +``` - +## 8. Query data from the app - +Use a `task` to fetch the data from the database and display it using a `List`. - ```swift name=Supabase.swift - import Supabase +Replace the default `ContentView` with the following code. - let supabase = SupabaseClient( - supabaseURL: URL(string: "YOUR_SUPABASE_URL")!, - supabaseKey: "YOUR_SUPABASE_PUBLISHABLE_KEY" - ) - ``` +```swift name=ContentView.swift +import SwiftUI - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} /> +struct ContentView: View { - + @State var instruments: [Instrument] = [] - - - - - - - Create a decodable struct to deserialize the data from the database. - - Add the following code to a new file named `Instrument.swift`. - - - - - - ```swift name=Instrument.swift - struct Instrument: Decodable, Identifiable { - let id: Int - let name: String + var body: some View { + List(instruments) { instrument in + Text(instrument.name) + } + .overlay { + if instruments.isEmpty { + ProgressView() } - ``` - - - - - - - - - - Use a `task` to fetch the data from the database and display it using a `List`. - - Replace the default `ContentView` with the following code. - - - - - - ```swift name=ContentView.swift - import SwiftUI - - struct ContentView: View { - - @State var instruments: [Instrument] = [] - - var body: some View { - List(instruments) { instrument in - Text(instrument.name) - } - .overlay { - if instruments.isEmpty { - ProgressView() - } - } - .task { - do { - instruments = try await supabase.from("instruments").select().execute().value - } catch { - dump(error) - } - } - } + } + .task { + do { + instruments = try await supabase.from("instruments").select().execute().value + } catch { + dump(error) } - ``` + } + } +} +``` - +## 9. Start the app - +Run the app on a simulator or a physical device by hitting `Cmd + R` on Xcode. - - - - Run the app on a simulator or a physical device by hitting `Cmd + R` on Xcode. - - - - - - - -## Setting up deep links +## 10. Setting up deep links (optional) If you want to implement authentication features like magic links or OAuth, you need to set up deep links to redirect users back to your app. For instructions on configuring custom URL schemes for your iOS app, see the [deep linking guide](/docs/guides/auth/native-mobile-deep-linking?platform=swift). diff --git a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx index bf300112b3f..6c9b2d1fa71 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx @@ -2,189 +2,155 @@ title: 'Use Supabase with Android Kotlin' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an Android Kotlin app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create an Android app with Android Studio - <$Partial path="quickstart_db_setup.mdx" /> +Select the **Android Studio > New > New Android Project** menu item. - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - Open Android Studio > New > New Android Project. +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install dependencies - +Open `build.gradle.kts` (app) file and add the serialization plugin, Ktor client, and Supabase client. - - Open `build.gradle.kts` (app) file and add the serialization plug, Ktor client, and Supabase client. +Replace the version placeholders `$kotlin_version` with the Kotlin version of the project, and `$supabase_version` and `$ktor_version` with the respective latest versions. - Replace the version placeholders `$kotlin_version` with the Kotlin version of the project, and `$supabase_version` and `$ktor_version` with the respective latest versions. + - The latest supabase-kt version can be found [here](https://github.com/supabase-community/supabase-kt/releases) and Ktor version can be found [here](https://ktor.io/docs/welcome.html). +You can find the latest supabase-kt version [on GitHub](https://github.com/supabase-community/supabase-kt/releases) and Ktor [in the Ktor documentation](https://ktor.io/docs/welcome.html). - +
- - ```kotlin - plugins { - ... - kotlin("plugin.serialization") version "$kotlin_version" - } - ... - dependencies { - ... - implementation(platform("io.github.jan-tennert.supabase:bom:$supabase_version")) - implementation("io.github.jan-tennert.supabase:postgrest-kt") - implementation("io.ktor:ktor-client-android:$ktor_version") - } - ``` +```kotlin +plugins { + ... + kotlin("plugin.serialization") version "$kotlin_version" +} +... +dependencies { + ... + implementation(platform("io.github.jan-tennert.supabase:bom:$supabase_version")) + implementation("io.github.jan-tennert.supabase:postgrest-kt") + implementation("io.ktor:ktor-client-android:$ktor_version") +} +``` - +## 6. Add internet access permission - +Add the following line to the `AndroidManifest.xml` file under the `manifest` tag and outside the `application` tag. - +```xml +... + +... +``` - - Add the following line to the `AndroidManifest.xml` file under the `manifest` tag and outside the `application` tag. - +## 7. Initialize the Supabase client - - ```xml - ... - - ... - ``` +You can create a Supabase client whenever you need to perform an API call. - +For a quick example, create a client at the top of the `MainActivity.kt` file below the imports. - +Replace the `supabaseUrl` and `supabaseKey` with your own, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=androidkotlin&tab=mobiles): - + - - You can create a Supabase client whenever you need to perform an API call. +```kotlin +import ... - For the sake of simplicity, we will create a client in the `MainActivity.kt` file at the top just below the imports. +val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "your_publishable_key" + ) { + install(Postgrest) +} +... +``` - Replace the `supabaseUrl` and `supabaseKey` with your own: +<$Partial path="api_settings.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} /> - - +## 8. Create a data model for instruments +Create a serializable data class to represent the data from the database. - +Add the following below the `createSupabaseClient` function in the `MainActivity.kt` file. - +```kotlin +@Serializable +data class Instrument( + val id: Int, + val name: String, +) +``` - ```kotlin - import ... +## 9. Query data from the app - val supabase = createSupabaseClient( - supabaseUrl = "https://xyzcompany.supabase.co", - supabaseKey = "your_publishable_key" - ) { - install(Postgrest) - } - ... - ``` +Use `LaunchedEffect` to fetch data from the database and display it in a `LazyColumn`. - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} /> +Replace the default `MainActivity` class with the following code. - + - +This example application makes a network request from the UI code. In production, you should use a `ViewModel` to separate the UI and data fetching logic. - + - - Create a serializable data class to represent the data from the database. +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + SupabaseTutorialTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + InstrumentsList() + } + } + } + } +} - Add the following below the `createSupabaseClient` function in the `MainActivity.kt` file. - +@Composable +fun InstrumentsList() { + var instruments by remember { mutableStateOf>(listOf()) } + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + instruments = supabase.from("instruments") + .select().decodeList() + } + } + LazyColumn { + items( + instruments, + key = { instrument -> instrument.id }, + ) { instrument -> + Text( + instrument.name, + modifier = Modifier.padding(8.dp), + ) + } + } +} +``` - - ```kotlin - @Serializable - data class Instrument( - val id: Int, - val name: String, - ) - ``` +## 10. Start the app - - - - - - - - Use `LaunchedEffect` to fetch data from the database and display it in a `LazyColumn`. - - Replace the default `MainActivity` class with the following code. - - Note that we are making a network request from our UI code. In production, you should probably use a `ViewModel` to separate the UI and data fetching logic. - - - - ```kotlin - class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - SupabaseTutorialTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - InstrumentsList() - } - } - } - } - } - - @Composable - fun InstrumentsList() { - var instruments by remember { mutableStateOf>(listOf()) } - LaunchedEffect(Unit) { - withContext(Dispatchers.IO) { - instruments = supabase.from("instruments") - .select().decodeList() - } - } - LazyColumn { - items( - instruments, - key = { instrument -> instrument.id }, - ) { instrument -> - Text( - instrument.name, - modifier = Modifier.padding(8.dp), - ) - } - } - } - ``` - - - - - - - Run the app on an emulator or a physical device by clicking the `Run app` button in Android Studio. - - - - - +Run the app on an emulator or a physical device by clicking the `Run app` button in Android Studio. diff --git a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx index 381caecf2e5..6c9e55acc60 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/laravel.mdx @@ -2,144 +2,97 @@ title: 'Use Supabase with Laravel' subtitle: 'Learn how to create a PHP Laravel project, connect it to your Supabase Postgres database, and configure user authentication.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - - +## 3. Create a Laravel project - Make sure your PHP and Composer versions are up to date, then use `composer create-project` to scaffold a new Laravel project. +Make sure your PHP and Composer versions are up to date, then use `composer create-project` to scaffold a new Laravel project. - See the [Laravel docs](https://laravel.com/docs/10.x/installation#creating-a-laravel-project) for more details. +See the [Laravel docs](https://laravel.com/docs/10.x/installation#creating-a-laravel-project) for more details. - +```bash +composer create-project laravel/laravel example-app +``` - +## 4. Install Agent Skills (optional) - ```bash name=Terminal - composer create-project laravel/laravel example-app - ``` +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - - +## 5. Install the authentication template - 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). - +```bash +composer require laravel/breeze --dev +php artisan breeze:install +``` - +## 6. Set up the Postgres connection details - ```bash name=Terminal - composer require laravel/breeze --dev - php artisan breeze:install - ``` +Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. - +When your project is up and running, navigate to your project dashboard and click on [Connect](/dashboard/project/_?showConnect=true&method=session). - +Look for the Session Pooler connection string and copy the string. You will need to replace the Password with your saved database password. You can reset your database password in your [Database Settings](/dashboard/project/_/database/settings) if you do not have it. - - + - Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. +If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. - When your project is up and running, navigate to your project dashboard and click on [Connect](/dashboard/project/_?showConnect=true&method=session). + - Look for the Session Pooler connection string and copy the string. You will need to replace the Password with your saved database password. You can reset your database password in your [Database Settings](/dashboard/project/_/database/settings) if you do not have it. +```bash name=.env +DB_CONNECTION=pgsql +DB_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres +``` - +## 7. Change the default schema - If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. +By default Laravel uses the `public` schema. We recommend changing this as Supabase exposes the `public` schema as a [data API](/docs/guides/api). - +You can change the schema of your Laravel application by modifying the `search_path` variable `app/config/database.php`. - +The schema you specify in `search_path` has to exist on Supabase. You can create a new schema from the [Table Editor](/dashboard/project/_/editor). - +```php name=app/config/database.php +'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'laravel', + 'sslmode' => 'prefer', +], +``` - ```bash name=.env - DB_CONNECTION=pgsql - DB_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres - ``` +## 8. Run the database migrations - +Laravel ships with database migration files that set up the required tables for Laravel Authentication and User Management. - +Note: Laravel does not use Supabase Auth but rather implements its own authentication system! - - +```bash +php artisan migrate +``` - By default Laravel uses the `public` schema. We recommend changing this as Supabase exposes the `public` schema as a [data API](/docs/guides/api). +## 9. Start the app - You can change the schema of your Laravel application by modifying the `search_path` variable `app/config/database.php`. +Run the development server. Go to http://127.0.0.1:8000 in a browser to see your application. You can also navigate to http://127.0.0.1:8000/register and http://127.0.0.1:8000/login to register and log in users. - The schema you specify in `search_path` has to exist on Supabase. You can create a new schema from the [Table Editor](/dashboard/project/_/editor). - - - - - - ```php name=app/config/database.php - 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DB_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'laravel'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => env('DB_CHARSET', 'utf8'), - 'prefix' => '', - 'prefix_indexes' => true, - 'search_path' => 'laravel', - 'sslmode' => 'prefer', - ], - ``` - - - - - - - - - Laravel ships with database migration files that set up the required tables for Laravel Authentication and User Management. - - Note: Laravel does not use Supabase Auth but rather implements its own authentication system! - - - - - - ```bash name=Terminal - php artisan migrate - ``` - - - - - - - - - Run the development server. Go to http://127.0.0.1:8000 in a browser to see your application. You can also navigate to http://127.0.0.1:8000/register and http://127.0.0.1:8000/login to register and log in users. - - - - - - ```bash name=Terminal - php artisan serve - ``` - - - - - - +```bash +php artisan serve +``` diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index fab3593778d..654823cecf8 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -2,128 +2,86 @@ title: 'Use Supabase with Next.js' subtitle: 'Learn how to create a Supabase project, add some sample data, and query from a Next.js app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Next.js app - <$Partial path="quickstart_db_setup.mdx" /> +Use the `create-next-app` command and the `with-supabase` template, to create a Next.js app pre-configured with [Cookie-based Auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server), [TypeScript](https://www.typescriptlang.org/), and [Tailwind CSS](https://tailwindcss.com/). - +```bash +npx create-next-app -e with-supabase +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - Use the `create-next-app` command and the `with-supabase` template, to create a Next.js app pre-configured with: - - [Cookie-based Auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server) - - [TypeScript](https://www.typescriptlang.org/) - - [Tailwind CSS](https://tailwindcss.com/) +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Declare Supabase environment variables - +Rename `.env.example` to `.env.local` and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nextjs&tab=frameworks). - ```bash - npx create-next-app -e with-supabase - ``` + - +```text name=.env.local +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} /> - - +## 6. Query Supabase data from Next.js - Rename `.env.example` to `.env.local` and populate with your Supabase connection variables: +Create a new file at `app/instruments/page.tsx` and populate with the following. - - +This selects all the rows from the `instruments` table you created earlier and renders them on the page. +<$CodeTabs> - +```ts name=app/instruments/page.tsx +import { createClient } from "@/lib/supabase/server"; +import { Suspense } from "react"; - +async function InstrumentsData() { + const supabase = await createClient(); + const { data: instruments } = await supabase.from("instruments").select(); - <$CodeTabs> + return
{JSON.stringify(instruments, null, 2)}
; +} - ```text name=.env.local - NEXT_PUBLIC_SUPABASE_URL= - NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= - ``` +export default function Instruments() { + return ( + Loading instruments...
}> + + + ); +} +``` - + - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} /> +## 7. Start the app - +Run the development server, go to http://localhost:3000/instruments in a browser and you should see the list of instruments. - - - - - - Create a new file at `app/instruments/page.tsx` and populate with the following. - - This selects all the rows from the `instruments` table in Supabase and render them on the page. - - - - - - <$CodeTabs> - - ```ts name=app/instruments/page.tsx - import { createClient } from "@/lib/supabase/server"; - import { Suspense } from "react"; - - async function InstrumentsData() { - const supabase = await createClient(); - const { data: instruments } = await supabase.from("instruments").select(); - - return
{JSON.stringify(instruments, null, 2)}
; - } - - export default function Instruments() { - return ( - Loading instruments...}> - - - ); - } - ``` - - - -
- -
- - - - - Run the development server, go to http://localhost:3000/instruments in a browser and you should see the list of instruments. - - - - - - ```bash Terminal - npm run dev - ``` - - - - - - +```bash +npm run dev +``` ## Next steps +- Explore [drop-in UI components](/ui) for your Supabase app - Set up [Auth](/docs/guides/auth) for your app - [Insert more data](/docs/guides/database/import-data) into your database - Upload and serve static files using [Storage](/docs/guides/storage) diff --git a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx index cb1ce765b79..c91850e0df3 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx @@ -2,148 +2,106 @@ title: 'Use Supabase with Nuxt' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Nuxt app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Nuxt app - <$Partial path="quickstart_db_setup.mdx" /> +Create a Nuxt app using the `npx nuxi` command. - +```bash +npx nuxi@latest init my-app +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - Create a Nuxt app using the `npx nuxi` command. +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - +The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Nuxt app. - ```bash name=Terminal - npx nuxi@latest init my-app - ``` +Navigate to the Nuxt app and install `supabase-js`. - +```bash +cd my-app && npm install @supabase/supabase-js +``` - +## 6. Declare Supabase environment variables - - +Create a `.env` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nuxt&tab=frameworks): - The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Nuxt app. + - Navigate to the Nuxt app and install `supabase-js`. +<$CodeTabs> - +```text name=.env +SUPABASE_URL= +SUPABASE_PUBLISHABLE_KEY= +``` - +```ts name=nuxt.config.ts +export default defineNuxtConfig({ + runtimeConfig: { + public: { + supabaseUrl: process.env.SUPABASE_URL, + supabasePublishableKey: process.env.SUPABASE_PUBLISHABLE_KEY, + }, + }, +}) +``` - ```bash name=Terminal - cd my-app && npm install @supabase/supabase-js - ``` + - +<$Partial path="api_settings.mdx" variables={{ "framework": "nuxt", "tab": "frameworks" }} /> - +## 7. Query data from the app - - +In `app.vue`, create a Supabase client using your config values and replace the existing content with the following code. - Create a `.env` file and populate with your Supabase connection variables: +```vue name=app.vue + - + +``` - <$CodeTabs> +## 8. Start the app - ```text name=.env.local - SUPABASE_URL= - SUPABASE_PUBLISHABLE_KEY= - ``` +Start the app, navigate to http://localhost:3000 in the browser, and you should see the list of instruments. - ```ts name=nuxt.config.tsx - export default defineNuxtConfig({ - runtimeConfig: { - public: { - supabaseUrl: process.env.SUPABASE_URL, - supabasePublishableKey: process.env.SUPABASE_PUBLISHABLE_KEY, - }, - }, - }); - ``` - - - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "nuxt", "tab": "frameworks" }} /> - - - - - - - - - In `app.vue`, create a Supabase client using your config values and replace the existing content with the following code. - - - - - - ```vue name=app.vue - - - - ``` - - - - - - - - - Start the app, navigate to http://localhost:3000 in the browser, open the browser console, and you should see the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - - +```bash +npm run dev +``` diff --git a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx index 66d9743c162..515e6267c68 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx @@ -2,152 +2,109 @@ title: 'Use Supabase with React' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a React app - <$Partial path="quickstart_db_setup.mdx" /> +Create a React app using a [Vite](https://vitejs.dev/guide/) template. - +```bash +npm create vite@latest my-app -- --template react +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - Create a React app using a [Vite](https://vitejs.dev/guide/) template. +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - +The fastest way to get started is to use the `supabase-js` client library, which provides a convenient interface for working with Supabase from a React app. - ```bash name=Terminal - npm create vite@latest my-app -- --template react - ``` +Navigate to the React app and install `supabase-js`. - +```bash +cd my-app && npm install @supabase/supabase-js +``` - +## 6. Declare Supabase environment variables - - +Create a `.env.local` file and populate it with your Supabase URL and publishable key that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=react&tab=frameworks) - The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a React app. + - Navigate to the React app and install `supabase-js`. +```text name=.env.local +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "react", "tab": "frameworks" }} /> - +## 7. Query data from the app - ```bash name=Terminal - cd my-app && npm install @supabase/supabase-js - ``` +Replace the contents of `App.jsx` with a `getInstruments` function that fetches the data and displays the query result on the page using a Supabase client. - +```js name=src/App.jsx +import { createClient } from '@supabase/supabase-js' +import { useEffect, useState } from 'react' - +const supabase = createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY +) - - +function App() { + const [instruments, setInstruments] = useState([]) - Create a `.env.local` file and populate with your Supabase connection variables: + useEffect(() => { + getInstruments() + }, []) - - + async function getInstruments() { + const { data, error } = await supabase.from('instruments').select() + if (error) { + console.error(error) + return + } - + setInstruments(data) + } - + return ( +
    + {instruments.map((instrument) => ( +
  • {instrument.name}
  • + ))} +
+ ) +} - <$CodeTabs> +export default App +``` - ```text name=.env.local - VITE_SUPABASE_URL= - VITE_SUPABASE_PUBLISHABLE_KEY= - ``` +## 8. Start the app - +Run the development server, go to http://localhost:5173 in a browser, and you should see the list of instruments. - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "react", "tab": "frameworks" }} /> - -
- -
- - - - - Replace the contents of `App.jsx` to add a `getInstruments` function to fetch the data and display the query result to the page using a Supabase client. - - - - - ```js name=src/App.jsx - import { useEffect, useState } from "react"; - import { createClient } from "@supabase/supabase-js"; - - const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY); - - function App() { - const [instruments, setInstruments] = useState([]); - - useEffect(() => { - getInstruments(); - }, []); - - async function getInstruments() { - const { data, error } = await supabase.from("instruments").select(); - - if (error) { - console.error(error); - return; - } - - setInstruments(data); - } - - return ( -
    - {instruments.map((instrument) => ( -
  • {instrument.name}
  • - ))} -
- ); - } - - export default App; - ``` - -
- -
- - - - - Run the development server, go to http://localhost:5173 in a browser and you should see the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - -
+```bash +npm run dev +``` ## Next steps +- Explore [drop-in UI components](/ui) for your Supabase app - Set up [Auth](/docs/guides/auth) for your app - [Insert more data](/docs/guides/database/import-data) into your database - Upload and serve static files using [Storage](/docs/guides/storage) diff --git a/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx index 3e8a6c42fc3..4bd411ef331 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/redwoodjs.mdx @@ -2,230 +2,168 @@ title: 'Use Supabase with RedwoodJS' subtitle: '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.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +## 1. Setup your new Supabase project - - - [Create a new project](/dashboard) in the Supabase Dashboard. +[Create a new project](/dashboard) in the Supabase Dashboard. - + - Be sure to make note of the Database Password you used as you will need this later to connect to your database. +Be sure to make note of the Database Password you used as you will need this later to connect to your database. - - +
- - ![New project for redwoodjs](/docs/img/guides/getting-started/quickstarts/redwoodjs/new-project.png) - +![New project for redwoodjs](/docs/img/guides/getting-started/quickstarts/redwoodjs/new-project.png) - +## 2. Gather database connection strings - - +Open the project [**Connect** panel](/dashboard/project/_?showConnect=true). This quickstart connects using the [**Transaction pooler**](/dashboard/project/_?showConnect=true&method=transaction) and [**Session pooler**](/dashboard/project/_?showConnect=true&method=session) mode. Transaction mode is used for application queries and Session mode is used for running migrations with Prisma. - Open the project [**Connect** panel](/dashboard/project/_?showConnect=true). This quickstart connects using the [**Transaction pooler**](/dashboard/project/_?showConnect=true&method=transaction) and [**Session pooler**](/dashboard/project/_?showConnect=true&method=session) mode. Transaction mode is used for application queries and Session mode is used for running migrations with Prisma. +To do this, set the connection mode to `Transaction` in the [Database Settings page](/dashboard/project/_/database/settings) and copy the connection string and append `?pgbouncer=true&connection_limit=1`. `pgbouncer=true` disables Prisma from generating prepared statements. This is required since our connection pooler does not support prepared statements in transaction mode yet. The `connection_limit=1` parameter is only required if you are using Prisma from a serverless environment. This is the Transaction mode connection string. - To do this, set the connection mode to `Transaction` in the [Database Settings page](/dashboard/project/_/database/settings) and copy the connection string and append `?pgbouncer=true&&connection_limit=1`. `pgbouncer=true` disables Prisma from generating prepared statements. This is required since our connection pooler does not support prepared statements in transaction mode yet. The `connection_limit=1` parameter is only required if you are using Prisma from a serverless environment. This is the Transaction mode connection string. +To get the Session mode connection pooler string, change the port of the connection string from the dashboard to 5432. - To get the Session mode connection pooler string, change the port of the connection string from the dashboard to 5432. +You will need the Transaction mode connection string and the Session mode connection string to set up environment variables in Step 6. - You will need the Transaction mode connection string and the Session mode connection string to setup environment variables in Step 5. + - +You can copy and paste these connection strings from the Supabase Dashboard when needed in later steps. - You can copy and paste these connection strings from the Supabase Dashboard when needed in later steps. + - - +![pooled connection for redwoodjs](/docs/img/guides/getting-started/quickstarts/redwoodjs/pooled-connection-strings.png) - - ![pooled connection for redwoodjs](/docs/img/guides/getting-started/quickstarts/redwoodjs/pooled-connection-strings.png) - +## 3. Create a RedwoodJS app - +Create a RedwoodJS app with TypeScript. - - - Create a RedwoodJS app with TypeScript. + - +The [`yarn` package manager](https://yarnpkg.com) is required to create a RedwoodJS app. You will use it to run RedwoodJS commands later. - The [`yarn` package manager](https://yarnpkg.com) is required to create a RedwoodJS app. You will use it to run RedwoodJS commands later. +While TypeScript is recommended, If you want a JavaScript app, omit the `--ts` flag. - While TypeScript is recommended, If you want a JavaScript app, omit the `--ts` flag. + - - +```bash +yarn create redwood-app my-app --ts +``` - - ```bash name=Terminal - yarn create redwood-app my-app --ts - ``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - - You'll develop your app, manage database migrations, and run your app in VS Code. - +To install, run the following command in the root of your project: - - ```bash name=Terminal - cd my-app - code . - ``` - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install MCP server (optional) - - +The Supabase MCP server connects AI assistants to Supabase, allowing you to interact with your projects on your behalf. Find out more on how to add it to your client in [the MCP docs](/docs/guides/ai-tools/mcp). - In your `.env` file, add the following environment variables for your database connection: +## 6. Configure environment variables - * The `DATABASE_URL` should use the Transaction mode connection string you copied in Step 1. +In your `.env` file, add the following environment variables for your database connection: - * The `DIRECT_URL` should use the Session mode connection string you copied in Step 1. +- The `DATABASE_URL` should use the Transaction mode connection string you copied in Step 2. - +- The `DIRECT_URL` should use the Session mode connection string you copied in Step 2. - - ```bash name=.env - # Transaction mode connection string used for migrations - DATABASE_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" +```bash name=.env +# Transaction mode connection string used for migrations +DATABASE_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" - # Session mode connection string — used by Prisma Client - DIRECT_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabase.com:5432/postgres" - ``` - +# Session mode connection string — used by Prisma Client +DIRECT_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabase.com:5432/postgres" +``` - +## 7. Update your Prisma schema - - - By default, RedwoodJS ships with a SQLite database, but we want to use Postgres. +By default, RedwoodJS ships with a SQLite database, but we want to use Postgres. - Update your Prisma schema file `api/db/schema.prisma` to use your Supabase Postgres database connection environment variables you setup in Step 5. - +Update your Prisma schema file `api/db/schema.prisma` to use your Supabase Postgres database connection environment variables you set up in Step 6. - - ```prisma name=api/db/schema.prisma - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") - } - ``` - +```prisma name=api/db/schema.prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} +``` - +## 8. Create the instrument model and apply a schema migration - - - Create the Instrument model in `api/db/schema.prisma` and then run `yarn rw prisma migrate dev` from your terminal to apply the migration. - +Create the Instrument model in `api/db/schema.prisma` and then run `yarn rw prisma migrate dev` from your terminal to apply the migration. - - ```prisma name=api/db/schema.prisma - model Instrument { - id Int @id @default(autoincrement()) - name String @unique - } - ``` - +```prisma name=api/db/schema.prisma +model Instrument { + id Int @id @default(autoincrement()) + name String @unique +} +``` - +## 9. Update seed script - - - Let's seed the database with a few instruments. +Seed the database with a few instruments. - Update the file `scripts/seed.ts` to contain the following code: - +Update the file `scripts/seed.ts` to contain the following code: - +```ts name=scripts/seed.ts +import type { Prisma } from '@prisma/client' +import { db } from 'api/src/lib/db' - ```ts name=scripts/seed.ts - import type { Prisma } from '@prisma/client' - import { db } from 'api/src/lib/db' +export default async () => { + try { + const data: Prisma.InstrumentCreateArgs['data'][] = [ + { name: 'dulcimer' }, + { name: 'harp' }, + { name: 'guitar' }, + ] - export default async () => { - try { - const data: Prisma.InstrumentCreateArgs['data'][] = [ - { name: 'dulcimer' }, - { name: 'harp' }, - { name: 'guitar' }, - ] + console.log('Seeding instruments ...') - console.log('Seeding instruments ...') + const instruments = await db.instrument.createMany({ data }) - const instruments = await db.instrument.createMany({ data }) + console.log('Done.', instruments) + } catch (error) { + console.error(error) + } +} +``` - console.log('Done.', instruments) - } catch (error) { - console.error(error) - } - } - ``` - +## 10. Seed your database - +Run the seed database command to populate the `Instrument` table with the instruments you created. - - - Run the seed database command to populate the `Instrument` table with the instruments you just created. + - +The reset database command `yarn rw prisma db reset` recreates the tables and also runs the seed script. - The reset database command `yarn rw prisma db reset` will recreate the tables and will also run the seed script. + - - +```bash +yarn rw prisma db seed +``` - - ```bash name=Terminal - yarn rw prisma db seed - ``` - +## 11. Scaffold the instrument UI - +Use RedwoodJS generators to scaffold a CRUD UI for the `Instrument` model. - - - Now, we'll use RedwoodJS generators to scaffold a CRUD UI for the `Instrument` model. - +```bash +yarn rw g scaffold instrument +``` - - ```bash name=Terminal - yarn rw g scaffold instrument - ``` - +## 12. Start the app - +Start the app via `yarn rw dev`. A browser will open to the RedwoodJS Splash page. - - - Start the app via `yarn rw dev`. A browser will open to the RedwoodJS Splash page. - +![RedwoodJS Splash Page](/docs/img/redwoodjs-qs-splash.png) - - ![RedwoodJS Splash Page](/docs/img/redwoodjs-qs-splash.png) +## 13. View instruments UI - +Click on `/instruments` to visit http://localhost:8910/instruments where should see the list of instruments. - - - - - Click on `/instruments` to visit http://localhost:8910/instruments where should see the list of instruments. - - You may now edit, delete, and add new books using the scaffolded UI. - - - - +You may now edit, delete, and add new instruments using the scaffolded UI. diff --git a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx index 1706c86e717..5fe26f5057a 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx @@ -2,224 +2,164 @@ title: 'Use Supabase with Refine' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Refine app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Refine app - <$Partial path="quickstart_db_setup.mdx" /> +Create a [Refine](https://github.com/refinedev/refine) app using the [create refine-app](https://refine.dev/docs/getting-started/quickstart/). - +The `refine-supabase` preset adds `@refinedev/supabase` supplementary package that supports Supabase in a Refine app. `@refinedev/supabase` out-of-the-box includes the Supabase dependency: [supabase-js](https://github.com/supabase/supabase-js). - +```bash +npm create refine-app@latest -- --preset refine-supabase my-app +``` - +![Refine welcome page](/docs/img/refine-qs-welcome-page.png) - Create a [Refine](https://github.com/refinedev/refine) app using the [create refine-app](https://refine.dev/docs/getting-started/quickstart/). +## 4. Install Agent Skills (optional) - The `refine-supabase` preset adds `@refinedev/supabase` supplementary package that supports Supabase in a Refine app. `@refinedev/supabase` out-of-the-box includes the Supabase dependency: [supabase-js](https://github.com/supabase/supabase-js). +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - ```bash name=Terminal - npm create refine-app@latest -- --preset refine-supabase my-app - ``` +## 5. Update `supabaseClient` with environment variables - +Update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=refine&tab=frameworks). The `supabaseClient` is used in auth provider and data provider methods that allow the Refine app to connect to your Supabase backend. - + - - +```ts name=src/utility/supabaseClient.ts +import { createClient } from '@refinedev/supabase' - You will develop your app, connect to the Supabase backend and run the Refine app in VS Code. +const SUPABASE_URL = '' +const SUPABASE_KEY = '' + +export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, { + db: { + schema: 'public', + }, + auth: { + persistSession: true, + }, +}) +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "refine", "tab": "frameworks" }} /> - +## 6. Add instruments resource and pages - ```bash name=Terminal - cd my-app - code . - ``` +Use the following code to automatically add resources and generate code for the pages to show the `instruments` data using Refine Inferencer. - +This defines pages for `list`, `create`, `show` and `edit` actions inside the `src/pages/instruments/` directory with a `` component. - +The `` component depends on `@refinedev/react-table` and `@refinedev/react-hook-form` packages. To avoid errors, you should install them as dependencies with `npm install @refinedev/react-table @refinedev/react-hook-form`. - - + - Start the app, go to http://localhost:5173 in a browser, and you should be greeted with the Refine Welcome page. +The `` is a Refine Inferencer component that automatically generates necessary code for the `list`, `create`, `show` and `edit` pages. - +Read more on [how the Inferencer works is in the Refine docs](https://refine.dev/docs/packages/documentation/inferencer/). - + - ```bash name=Terminal - npm run dev - ``` +```bash +npm run refine create-resource instruments +``` - - ![Refine welcome page](/docs/img/refine-qs-welcome-page.png) - +## 7. Add routes for instruments pages - +Add routes for the `list`, `create`, `show`, and `edit` pages. - + - - +Remove the `index` route for the Welcome page presented with the `` component. - You now have to update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API. The `supabaseClient` is used in auth provider and data provider methods that allow the Refine app to connect to your Supabase backend. + - - +```tsx name=src/App.tsx +import { Refine } from '@refinedev/core' +import { RefineKbar, RefineKbarProvider } from '@refinedev/kbar' +import routerProvider, { + DocumentTitleHandler, + NavigateToResource, + UnsavedChangesNotifier, +} from '@refinedev/react-router' +import { dataProvider, liveProvider } from '@refinedev/supabase' +import { BrowserRouter, Route, Routes } from 'react-router-dom' +import './App.css' - +import authProvider from './authProvider' +import { + InstrumentsCreate, + InstrumentsEdit, + InstrumentsList, + InstrumentsShow, +} from './pages/instruments' +import { supabaseClient } from './utility' - - - ```ts name=src/utility/supabaseClient.ts - import { createClient } from "@refinedev/supabase"; - - const SUPABASE_URL = YOUR_SUPABASE_URL; - const SUPABASE_KEY = YOUR_SUPABASE_KEY - - export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, { - db: { - schema: "public", - }, - auth: { - persistSession: true, - }, - }); - ``` - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "refine", "tab": "frameworks" }} /> - - - - - - - - - You have to then configure resources and define pages for `instruments` resource. - - Use the following command to automatically add resources and generate code for pages for `instruments` using Refine Inferencer. - - This defines pages for `list`, `create`, `show` and `edit` actions inside the `src/pages/instruments/` directory with `` component. - - The `` component depends on `@refinedev/react-table` and `@refinedev/react-hook-form` packages. In order to avoid errors, you should install them as dependencies with `npm install @refinedev/react-table @refinedev/react-hook-form`. - - - - The `` is a Refine Inferencer component that automatically generates necessary code for the `list`, `create`, `show` and `edit` pages. - - More on [how the Inferencer works is available in the docs here](https://refine.dev/docs/packages/documentation/inferencer/). - - - - - - - - - ```bash name=Terminal - npm run refine create-resource instruments - ``` - - - - - - - - Add routes for the `list`, `create`, `show`, and `edit` pages. - - - - You should remove the `index` route for the Welcome page presented with the `` component. - - - - - - - ```tsx name=src/App.tsx - import { Refine } from "@refinedev/core"; - import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar"; - import routerProvider, { - DocumentTitleHandler, - NavigateToResource, - UnsavedChangesNotifier, - } from "@refinedev/react-router"; - import { dataProvider, liveProvider } from "@refinedev/supabase"; - import { BrowserRouter, Route, Routes } from "react-router-dom"; - - import "./App.css"; - import authProvider from "./authProvider"; - import { supabaseClient } from "./utility"; - import { InstrumentsCreate, InstrumentsEdit, InstrumentsList, InstrumentsShow } from "./pages/instruments"; - - function App() { - return ( - - - - - } - /> - - } /> - } /> - } /> - } /> - - - - - - - - - ); - } - - export default App; - ``` - - - - - - - - Now you should be able to see the instruments pages along the `/instruments` routes. You may now edit and add new instruments using the Inferencer generated UI. - - The Inferencer auto-generated code gives you a good starting point on which to keep building your `list`, `create`, `show` and `edit` pages. They can be obtained by clicking the `Show the auto-generated code` buttons in their respective pages. - - - - +function App() { + return ( + + + + + } /> + + } /> + } /> + } /> + } /> + + + + + + + + + ) +} + +export default App +``` + +## 8. View instruments pages + +Start the app with the following command: + +```bash +npm run dev +``` + +Open http://localhost:5173/instruments in a browser, and you should be able to see the instruments pages along the `/instruments` routes. You can edit and add new instruments using the Inferencer generated UI. + +The Inferencer auto-generated code gives you a good starting point on which to keep building your `list`, `create`, `show` and `edit` pages. You can get these by clicking the `Show the auto-generated code` buttons in their respective pages. diff --git a/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx b/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx index bec7d960cfd..f1d6a8aef66 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/ruby-on-rails.mdx @@ -2,116 +2,80 @@ title: 'Use Supabase with Ruby on Rails' subtitle: 'Learn how to create a Rails project and connect it to your Supabase Postgres database.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +## 1. Create a Rails project - - +With your Ruby and Rails versions up to date, run `rails new` on your terminal to scaffold a new project. - Make sure your Ruby and Rails versions are up to date, then use `rails new` to scaffold a new Rails project. Use the `-d=postgresql` flag to set it up for Postgres. +Use the `-d=postgresql` flag to set it up for Postgres. - Go to the [Rails docs](https://guides.rubyonrails.org/getting_started.html) for more details. +Check the [Rails docs](https://guides.rubyonrails.org/getting_started.html) for more details. - +```bash +rails new blog -d=postgresql +``` - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - ```bash name=Terminal - rails new blog -d=postgresql - ``` +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 3. Install MCP server (optional) - - +The Supabase MCP server connects AI assistants to Supabase, allowing you to interact with your projects on your behalf. Find out more on how to add it to your client in [the MCP docs](/docs/guides/ai-tools/mcp). - Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. +## 4. Set up the Postgres connection details - When your project is up and running, navigate to your project dashboard and click on [Connect](/dashboard/project/_?showConnect=true&method=session). +Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely. - Look for the Session Pooler connection string and copy the string. You will need to replace the Password with your saved database password. You can reset your database password in your [Database Settings](/dashboard/project/_/database/settings) if you do not have it. +When your project is up and running, navigate to your project dashboard and click on [Connect](/dashboard/project/_?showConnect=true&method=session). - +Look for the Session Pooler connection string and copy the string. You will need to replace the Password with your saved database password. You can reset your database password in your [Database Settings](/dashboard/project/_/database/settings) if you do not have it. - If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. + - +If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode. - + - +```bash name=.env +export DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres +``` - ```bash name=Terminal - export DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres - ``` +## 5. Create and run a database migration - +Rails includes Active Record as the ORM as well as database migration tooling which generates the SQL migration files for you. - +Create an example `Article` model and generate the migration files. - - +```bash +bin/rails generate model Article title:string body:text +bin/rails db:migrate +``` - Rails includes Active Record as the ORM as well as database migration tooling which generates the SQL migration files for you. +## 6. Use the model to interact with the database - Create an example `Article` model and generate the migration files. +You can use the included Rails console to interact with the database. For example, you can create new entries or list all entries in a Model's table. - +```bash +bin/rails console +``` - +```rb name=irb +article = Article.new(title: "Hello Rails", body: "I am on Rails!") +article.save # Saves the entry to the database - ```bash name=Terminal - bin/rails generate model Article title:string body:text - bin/rails db:migrate - ``` +Article.all +``` - +## 7. Start the app - +Run the development server. Go to http://127.0.0.1:3000 in a browser to see your application running. - - - - You can use the included Rails console to interact with the database. For example, you can create new entries or list all entries in a Model's table. - - - - - - ```bash name=Terminal - bin/rails console - ``` - - ```rb name=irb - article = Article.new(title: "Hello Rails", body: "I am on Rails!") - article.save # Saves the entry to the database - - Article.all - ``` - - - - - - - - - Run the development server. Go to http://127.0.0.1:3000 in a browser to see your application running. - - - - - - ```bash name=Terminal - bin/rails server - ``` - - - - - - +```bash +bin/rails server +``` diff --git a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx index 1d5d01fbda5..6ac87800239 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx @@ -2,135 +2,92 @@ title: 'Use Supabase with SolidJS' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a SolidJS app - <$Partial path="quickstart_db_setup.mdx" /> +Create a SolidJS app using the `degit` command. - +```bash +npx degit solidjs/templates/js my-app +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - Create a SolidJS app using the `degit` command. +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - ```bash name=Terminal - npx degit solidjs/templates/js my-app - ``` +The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SolidJS app. - +Navigate to the SolidJS app and install `supabase-js`. - +```bash +cd my-app && npm install @supabase/supabase-js +``` - - +## 6. Declare Supabase environment variables - The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SolidJS app. +Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=solidjs&tab=frameworks): - Navigate to the SolidJS app and install `supabase-js`. + - +```text name=.env.local +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "solidjs", "tab": "frameworks" }} /> - ```bash name=Terminal - cd my-app && npm install @supabase/supabase-js - ``` +## 7. Query data from the app - +In `App.jsx`, create a Supabase client to fetch the instruments data. - +Add a `getInstruments` function to fetch the data and display the query result to the page. - - +```jsx name=src/App.jsx +import { createClient } from '@supabase/supabase-js' +import { createResource, For } from 'solid-js' - Create a `.env.local` file and populate with your Supabase connection variables: +const supabase = createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY +) - - +async function getInstruments() { + const { data } = await supabase.from('instruments').select() + return data +} +function App() { + const [instruments] = createResource(getInstruments) - + return ( +
    + {(instrument) =>
  • {instrument.name}
  • }
    +
+ ) +} - +export default App +``` - <$CodeTabs> +## 8. Start the app - ```text name=.env.local - VITE_SUPABASE_URL= - VITE_SUPABASE_PUBLISHABLE_KEY= - ``` +Start the app and go to http://localhost:3000 in a browser and you should see the list of instruments. - - - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "solidjs", "tab": "frameworks" }} /> - - - -
- - - - - In `App.jsx`, create a Supabase client to fetch the instruments data. - - Add a `getInstruments` function to fetch the data and display the query result to the page. - - - - - - ```jsx name=src/App.jsx - import { createClient } from "@supabase/supabase-js"; - import { createResource, For } from "solid-js"; - - const supabase = createClient('https://.supabase.co', ''); - - async function getInstruments() { - const { data } = await supabase.from("instruments").select(); - return data; - } - - function App() { - const [instruments] = createResource(getInstruments); - - return ( -
    - {(instrument) =>
  • {instrument.name}
  • }
    -
- ); - } - - export default App; - ``` - -
- -
- - - - - Start the app and go to http://localhost:3000 in a browser and you should see the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - -
+```bash +npm run dev +``` diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index 16fff4527d8..669079174bc 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -2,204 +2,143 @@ title: 'Use Supabase with SvelteKit' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SvelteKit app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a SvelteKit app - <$Partial path="quickstart_db_setup.mdx" /> +Create a SvelteKit app using the `npm create` command. - +```bash +npx sv create my-app +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - Create a SvelteKit app using the `npm create` command. +To install, run the following command in the root of your project: - +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - ```bash name=Terminal - npx sv create my-app - ``` +The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SvelteKit app. - +Navigate to the SvelteKit app and install `supabase-js`. - +```bash +cd my-app && npm install @supabase/supabase-js +``` - - +## 6. Declare Supabase environment variables - The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SvelteKit app. +Create a `.env` file at the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=sveltekit&tab=frameworks): - Navigate to the SvelteKit app and install `supabase-js`. + - +```text name=.env +PUBLIC_SUPABASE_URL= +PUBLIC_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "sveltekit", "tab": "frameworks" }} /> - ```bash name=Terminal - cd my-app && npm install @supabase/supabase-js - ``` +## 7. Create the Supabase client - +Create a `src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client: - +<$CodeTabs> - +```js name=src/lib/supabaseClient.js +import { createClient } from '@supabase/supabase-js' +import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' - +export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY) +``` - Create a `.env` file at the root of your project and populate with your Supabase connection variables: +```ts name=src/lib/supabaseClient.ts +import { createClient } from '@supabase/supabase-js' +import { PUBLIC_SUPABASE_PUBLISHABLE_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public' - - +export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY) +``` + - +## 8. Query data from the app - +Use `load` method to fetch the data server-side and display the query results as a list. - <$CodeTabs> +Create `+page.server.js` file in the `src/routes` directory with the following code. - ```text name=.env - PUBLIC_SUPABASE_URL= - PUBLIC_SUPABASE_PUBLISHABLE_KEY= - ``` +<$CodeTabs> - - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "sveltekit", "tab": "frameworks" }} /> +```js name=src/routes/+page.server.js +import { supabase } from '$lib/supabaseClient' - +export async function load() { + const { data } = await supabase.from('instruments').select() + return { + instruments: data ?? [], + } +} +``` - +```ts name=src/routes/+page.server.ts +import { supabase } from '$lib/supabaseClient' - - +import type { PageServerLoad } from './$types' - Create a `src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client: +type Instrument = { + id: number + name: string +} - +export const load: PageServerLoad = async () => { + const { data, error } = await supabase.from('instruments').select<'instruments', Instrument>() - + if (error) { + console.error('Error loading instruments:', error.message) + return { instruments: [] } + } - <$CodeTabs> + return { + instruments: data ?? [], + } +} +``` - ```js name=src/lib/supabaseClient.js - import { createClient } from '@supabase/supabase-js'; - import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; + - export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY) - ``` +Replace the existing content in your `+page.svelte` file in the `src/routes` directory with the following code. - ```ts name=src/lib/supabaseClient.ts - import { createClient } from '@supabase/supabase-js'; - import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$env/static/public'; +```svelte name=src/routes/+page.svelte + - export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY) - ``` +
    + {#each data.instruments as instrument} +
  • {instrument.name}
  • + {/each} +
+``` - +## 9. Start the app -
+Start the app and go to http://localhost:5173 in a browser and you should see the list of instruments. -
- - - - - Use `load` method to fetch the data server-side and display the query results as a simple list. - - Create `+page.server.js` file in the `src/routes` directory with the following code. - - - - - <$CodeTabs> - - ```js name=src/routes/+page.server.js - import { supabase } from "$lib/supabaseClient"; - - export async function load() { - const { data } = await supabase.from("instruments").select(); - return { - instruments: data ?? [], - }; - } - ``` - - ```ts name=src/routes/+page.server.ts - import type { PageServerLoad } from './$types'; - import { supabase } from '$lib/supabaseClient'; - - type Instrument = { - id: number; - name: string; - }; - - export const load: PageServerLoad = async () => { - const { data, error } = await supabase.from('instruments').select<'instruments', Instrument>(); - - if (error) { - console.error('Error loading instruments:', error.message); - return { instruments: [] }; - } - - return { - instruments: data ?? [], - }; - }; - ``` - - - - - - - - Replace the existing content in your `+page.svelte` file in the `src/routes` directory with the following code. - - - - - - ```svelte name=src/routes/+page.svelte - - -
    - {#each data.instruments as instrument} -
  • {instrument.name}
  • - {/each} -
- ``` - -
- -
- - - - - Start the app and go to http://localhost:5173 in a browser and you should see the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - -
+```bash +npm run dev +``` ## Next steps diff --git a/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx b/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx index 569ac3591ae..b59f80d39ad 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx @@ -2,163 +2,107 @@ title: 'Use Supabase with TanStack Start' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a TanStack Start app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a TanStack Start app - <$Partial path="quickstart_db_setup.mdx" /> +Create a TanStack Start app using the official CLI. - +```bash +npm create @tanstack/start@latest my-app -- --package-manager npm --toolchain biome +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - Create a TanStack Start app using the official CLI. +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - +The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a TanStack Start app. - ```bash name=Terminal - npm create @tanstack/start@latest my-app -- --package-manager npm --toolchain biome - ``` +Navigate to the TanStack Start app and install `supabase-js`. - +```bash +cd my-app && npm install @supabase/supabase-js +``` - +## 6. Declare Supabase environment variables - - +Create a `.env` file in the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true): - The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a TanStack Start app. + - Navigate to the TanStack Start app and install `supabase-js`. +```text name=.env +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} /> - +## 7. Create a Supabase client utility - ```bash name=Terminal - cd my-app && npm install @supabase/supabase-js - ``` +Create a new file at `src/utils/supabase.ts` to initialize the Supabase client. - +```ts name=src/utils/supabase.ts +import { createClient } from '@supabase/supabase-js' - +export const supabase = createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY +) +``` - - +## 8. Query data from the app - Create a `.env` file in the root of your project and populate with your Supabase connection variables: +Replace the contents of `src/routes/index.tsx` with the following code to add a loader function that fetches the instruments data and displays it on the page. - - +```tsx name=src/routes/index.tsx +import { createFileRoute } from '@tanstack/react-router' +import { supabase } from '../utils/supabase' - +export const Route = createFileRoute('/')({ + loader: async () => { + const { data: instruments } = await supabase.from('instruments').select() + return { instruments } + }, + component: Home, +}) - +function Home() { + const { instruments } = Route.useLoaderData() - <$CodeTabs> + return ( +
    + {instruments?.map((instrument) => ( +
  • {instrument.name}
  • + ))} +
+ ) +} +``` - ```text name=.env - VITE_SUPABASE_URL= - VITE_SUPABASE_PUBLISHABLE_KEY= - ``` +## 9. Start the app - +Run the development server, go to http://localhost:3000 in a browser and you should see the list of instruments. - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "", "tab": "" }} /> - -
- -
- - - - - Create a new file at `src/utils/supabase.ts` to initialize the Supabase client. - - - - - - ```ts name=src/utils/supabase.ts - import { createClient } from "@supabase/supabase-js"; - - export const supabase = createClient( - import.meta.env.VITE_SUPABASE_URL, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY - ); - ``` - - - - - - - - - Replace the contents of `src/routes/index.tsx` with the following code to add a loader function that fetches the instruments data and displays it on the page. - - - - - ```tsx name=src/routes/index.tsx - import { createFileRoute } from '@tanstack/react-router' - import { supabase } from '../utils/supabase' - - export const Route = createFileRoute('/')({ - loader: async () => { - const { data: instruments } = await supabase.from('instruments').select() - return { instruments } - }, - component: Home, - }) - - function Home() { - const { instruments } = Route.useLoaderData() - - return ( -
    - {instruments?.map((instrument) => ( -
  • {instrument.name}
  • - ))} -
- ) - } - ``` - -
- -
- - - - - Run the development server, go to http://localhost:3000 in a browser and you should see the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - -
+```bash +npm run dev +``` ## Next steps +- Explore [drop-in UI components](/ui) for your Supabase app - Set up [Auth](/docs/guides/auth) for your app - [Insert more data](/docs/guides/database/import-data) into your database - Upload and serve static files using [Storage](/docs/guides/storage) diff --git a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx index 269233acf16..6e34ba1244e 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx @@ -2,156 +2,101 @@ title: 'Use Supabase with Vue' subtitle: 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app.' breadcrumb: 'Framework Quickstarts' -hideToc: true --- - +<$Partial path="quickstart_db_setup.mdx" /> - +## 3. Create a Vue app - <$Partial path="quickstart_db_setup.mdx" /> +Create a Vue app using the `npm init` command. - +```sh +npm init vue@latest my-app +``` - +## 4. Install Agent Skills (optional) - +Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase. - - Create a Vue app using the `npm init` command. +To install, run the following command in the root of your project: - <$Partial path="uiLibCta.mdx" /> +```bash +npx skills add supabase/agent-skills +``` - +## 5. Install the Supabase client library - +The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Vue app. - ```sh name=Terminal - npm init vue@latest my-app - ``` +Navigate to the Vue app and install `supabase-js`. - +```bash +cd my-app && npm install @supabase/supabase-js +``` - +## 6. Declare Supabase environment variables - - +Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=vue&tab=frameworks): - The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Vue app. + - Navigate to the Vue app and install `supabase-js`. +```text name=.env.local +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` - +<$Partial path="api_settings.mdx" variables={{ "framework": "vue", "tab": "frameworks" }} /> - +## 7. Create the Supabase client - ```bash name=Terminal - cd my-app && npm install @supabase/supabase-js - ``` +Create a `/src/lib` directory in your Vue app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client: - +```js name=src/lib/supabaseClient.js +import { createClient } from '@supabase/supabase-js' - +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL +const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY - - +export const supabase = createClient(supabaseUrl, supabasePublishableKey) +``` - Create a `.env.local` file and populate with your Supabase connection variables: +## 8. Query data from the app - - +Replace the existing content in your `App.vue` file with the following code. +```vue name=src/App.vue + - - <$Partial path="api_settings_steps.mdx" variables={{ "framework": "vue", "tab": "frameworks" }} /> + +``` - +## 9. Start the app - +Start the app and go to http://localhost:5173 in a browser and you should see the list of instruments. - - - - Create a `/src/lib` directory in your Vue app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client: - - - - - - ```js name=src/lib/supabaseClient.js - import { createClient } from '@supabase/supabase-js' - - const supabaseUrl = import.meta.env.VITE_SUPABASE_URL - const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY - - export const supabase = createClient(supabaseUrl, supabasePublishableKey) - ``` - - - - - - - - - Replace the existing content in your `App.vue` file with the following code. - - - - - - ```vue name=src/App.vue - - - - ``` - - - - - - - - - Start the app and go to http://localhost:5173 in a browser and you should see the list of instruments. - - - - - - ```bash name=Terminal - npm run dev - ``` - - - - - +```bash +npm run dev +``` 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 9c345a4d4dd..19ea149237c 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,9 +41,9 @@ 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. +We can setup deep links with a minor tweak on our Flutter application. We have to use `io.supabase.flutterquickstart` as the scheme. In this example, we will use `login-callback` as the host for our deep link, but you can change it to whatever you would like. @@ -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,7 +310,7 @@ 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> @@ -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-ionic-vue.mdx b/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx index baff725b244..fcf2908a826 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-ionic-vue.mdx @@ -78,6 +78,8 @@ meta="name=src/views/Account.vue" With all the components in place, update `App.vue` and the app routes: +<$Partial path="auth_methods.mdx" /> + <$CodeSample path="/user-management/ionic-vue-user-management/src/router/index.ts" lines={[[1, -1]]} 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 f7fd26e7894..bc3467a8f54 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-nextjs.mdx @@ -53,7 +53,7 @@ You can find the full contents of this file [in the example repository](https:// Next.js is a versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and proxy edge-functions. -To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to quickly configure your Supabase project to use cookies for storing user sessions. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs) for more information. +To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to configure your Supabase project to use cookies for storing user sessions. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs) for more information. Install the package for Next.js. @@ -216,7 +216,7 @@ lines={[[1, 4], [7, 78], [88, 89], [99, -1]]} meta="name=app/account/account-form.tsx" /> -Create an account page for the `AccountForm` component you just created +Create an account page for the `AccountForm` component you created <$CodeSample path="/user-management/nextjs-user-management/app/account/page.tsx" 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..cb0774b4cd2 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 @@ -115,7 +115,7 @@ SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET -And finally, you will also need to save **just** the `web side` environment variables to the `redwood.toml`. +And finally, you will also need to save **only** the `web side` environment variables to the `redwood.toml`. <$CodeTabs> @@ -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 / @@ -207,7 +207,7 @@ The `/` is important here as it creates a root level route. -You can stop the `dev` server if you want; to see your changes, just be sure to run `yarn rw dev` again. +You can stop the `dev` server if you want; to see your changes, run `yarn rw dev` again. You should see the `Home` page route in `web/src/Routes.js`: @@ -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 74ff3e28eeb..6df0fa75f51 100644 --- a/apps/docs/content/guides/integrations/partner-integration-guide.mdx +++ b/apps/docs/content/guides/integrations/partner-integration-guide.mdx @@ -9,12 +9,12 @@ 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. @@ -55,6 +55,8 @@ sequenceDiagram Browser->>User: Display "Integration Installed" Message ``` +The user clicks **Install Integration** in the Supabase Dashboard, which routes them through the partner's click-handler page and on to the Supabase authorization page. Supabase shows a consent screen. Once the user consents, Supabase redirects back to the partner with an authorization code. The partner exchanges that code for a token, uses the token to request Management API resources, and finally shows the user an "Integration Installed" message. + ### Step 1: Implement the redirect endpoint Expose a `GET` endpoint at any URL you control: @@ -136,6 +138,8 @@ sequenceDiagram Browser->>User: Display "Integration Installed" Message ``` +Walking through the sequence: when the user clicks **Install Integration**, Supabase sends the partner a signed JWT and asks it to generate a redirect URL. The partner validates the JWT, generates a unique redirect record and URL, and returns it. Supabase redirects the user to that URL; the partner validates the redirect record, optionally has the user complete extra steps, and then sends them to the Supabase authorization page. From there the flow matches Method 1: the user consents, Supabase returns an authorization code, the partner exchanges it for a token, requests Management API resources, and the integration completes. + ### Step 1: Exchange public keys with Supabase Supabase generates two key-pairs, one for staging, one for production, and shares the public keys with you. Save both public keys and their key IDs in your system. The keys are PEM-encoded EC P-256. diff --git a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx index c1ac2b31012..45be844a97f 100644 --- a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx +++ b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx @@ -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..ac3c21bbf61 100644 --- a/apps/docs/content/guides/integrations/vercel-marketplace.mdx +++ b/apps/docs/content/guides/integrations/vercel-marketplace.mdx @@ -8,7 +8,7 @@ description: 'Manage your Supabase projects directly through Vercel' The Vercel Marketplace is a feature that allows you to manage third-party resources, such as Supabase, directly from the Vercel platform. This integration offers a seamless experience with unified billing, streamlined authentication, and easy access management for your team. -When you create an organization and projects through Vercel Marketplace, they function just like those created directly within Supabase. However, the billing is handled through your Vercel account, and you can manage your resources directly from the Vercel dashboard or CLI. Additionally, environment variables are automatically synchronized, making them immediately available for your connected projects. +When you create an organization and projects through Vercel Marketplace, they function like those created directly within Supabase. However, the billing is handled through your Vercel account, and you can manage your resources directly from the Vercel dashboard or CLI. Additionally, environment variables are automatically synchronized, making them immediately available for your connected projects. For more information, see [Introducing the Vercel Marketplace](https://vercel.com/blog/introducing-the-vercel-marketplace) blog post. @@ -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 8b531f49e4b..10838693b14 100644 --- a/apps/docs/content/guides/local-development/cli/getting-started.mdx +++ b/apps/docs/content/guides/local-development/cli/getting-started.mdx @@ -4,7 +4,7 @@ description: 'The Supabase CLI provides tools to develop your project locally, d subtitle: 'Develop locally, deploy to the Supabase Platform, and set up CI/CD workflows' --- -The Supabase CLI enables you to run the entire Supabase stack locally, on your machine or in a CI environment. With just two commands, you can set up and start a new local project: +The Supabase CLI enables you to run the entire Supabase stack locally, on your machine or in a CI environment. With two commands, you can set up and start a new local project: 1. `supabase init` to create a new local project 2. `supabase start` to launch the Supabase services @@ -58,7 +58,7 @@ and run one of the following: - `sudo rpm -i <...>.rpm` - + Run the CLI by prefixing each command with `npx` or `bunx`: @@ -122,7 +122,7 @@ brew link --overwrite supabase-beta Beta builds are attached to [GitHub pre-releases](https://github.com/supabase/cli/releases). Download the `.apk`, `.deb`, or `.rpm` for your platform and install with the same commands as [Linux packages](#linux-packages) above. - + Install as a dev dependency: @@ -199,7 +199,7 @@ brew upgrade supabase-beta - `sudo rpm -i <...>.rpm` - + If you have installed the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase), you can update it with: 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 017961afb7c..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 @@ -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/customizing-email-templates.mdx b/apps/docs/content/guides/local-development/customizing-email-templates.mdx index 7316d957249..d0ad4d727b9 100644 --- a/apps/docs/content/guides/local-development/customizing-email-templates.mdx +++ b/apps/docs/content/guides/local-development/customizing-email-templates.mdx @@ -6,6 +6,8 @@ subtitle: 'Customize local email templates via the config file.' You can customize the email templates for local development by [editing the `config.toml` file](/docs/guides/local-development/cli/config#auth-config). +This guide covers local development and CLI workflows. For hosted projects, use the [Email Templates](/dashboard/project/_/auth/templates) page in the dashboard. See [Email templates](/docs/guides/auth/auth-email-templates) for terminology, limitations, and customization patterns that apply in every environment. + For configuring a self-hosted Supabase instance, see [Custom Email Templates](/docs/guides/self-hosting/custom-email-templates) @@ -104,7 +106,7 @@ There are several authentication-related email templates which can be configured **Default subject**: "`{{ .Token }} is your verification code`" **When sent**: When a user needs to re-authenticate for sensitive operations **Purpose**: Ask users to verify their identity before a sensitive operation -**Content**: Contains a 6-digit OTP code for verification +**Content**: Contains a 8-digit OTP code for verification ## Available security notification email templates @@ -179,7 +181,7 @@ https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=email ### `Token` -Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `ConfirmationURL`. +Contains a 8-digit One-Time-Password (OTP) that can be used instead of the `ConfirmationURL`. **Usage** diff --git a/apps/docs/content/guides/local-development/overview.mdx b/apps/docs/content/guides/local-development/overview.mdx index 708a4d065c0..0c2b6f00169 100644 --- a/apps/docs/content/guides/local-development/overview.mdx +++ b/apps/docs/content/guides/local-development/overview.mdx @@ -7,7 +7,7 @@ video: 'https://www.youtube-nocookie.com/v/vyHyYpvjaks' tocVideo: 'vyHyYpvjaks' --- -Supabase is a flexible platform that lets you decide how you want to build your projects. You can use the Dashboard directly to get up and running quickly, or use a proper local setup. We suggest you work locally and deploy your changes to a linked project on the [Supabase Platform](https://app.supabase.io/). +Supabase is a flexible platform that lets you decide how you want to build your projects. You can use the Dashboard directly to get up and running, or use a proper local setup. We suggest you work locally and deploy your changes to a linked project on the [Supabase Platform](https://app.supabase.io/). Develop locally using the CLI to run a local Supabase stack. You can use the integrated Studio Dashboard to make changes, then capture your changes in schema migration files, which can be saved in version control. diff --git a/apps/docs/content/guides/local-development/seeding-your-database.mdx b/apps/docs/content/guides/local-development/seeding-your-database.mdx index 4426ea1f7f8..48e55d531a2 100644 --- a/apps/docs/content/guides/local-development/seeding-your-database.mdx +++ b/apps/docs/content/guides/local-development/seeding-your-database.mdx @@ -98,7 +98,31 @@ export default defineConfig({ Suppose you have a database with the following schema: -![An example schema](/docs/img/guides/cli/snaplet-example-schema.png) +```mermaid +erDiagram + User ||--o{ Post : createdBy + User ||--o{ Comment : userId + Post ||--o{ Comment : postId + User { + bigint id PK + text email + text name + } + Post { + bigint id PK + text title + text content + bigint createdBy FK + } + Comment { + bigint id PK + text text + bigint userId FK + bigint postId FK + } +``` + +This example schema has three tables. A `User` can author many `Post` rows (`Post.createdBy` references `User.id`) and many `Comment` rows (`Comment.userId` references `User.id`), and each `Post` can have many `Comment` rows (`Comment.postId` references `Post.id`). In other words, users create posts and comments, and every comment belongs to a post. You can use the seed script example generated by Snaplet `seed.ts` to define the values you want to generate. For example: @@ -107,8 +131,8 @@ You can use the seed script example generated by Snaplet `seed.ts` to define the - Three `Post.comments` from three different users. ```ts seed.ts -import { createSeedClient } from '@snaplet/seed' import { copycat } from '@snaplet/copycat' +import { createSeedClient } from '@snaplet/seed' async function main() { const seed = await createSeedClient({ dryRun: true }) 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/backups.mdx b/apps/docs/content/guides/platform/backups.mdx index 9a2123b6f4c..9be4bdb6459 100644 --- a/apps/docs/content/guides/platform/backups.mdx +++ b/apps/docs/content/guides/platform/backups.mdx @@ -91,13 +91,11 @@ Projects that want to use PITR must also use at least a Small compute add-on to -
-
diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 24d8cd58770..83906fe29cd 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. @@ -96,11 +96,11 @@ We currently do not support annual plans officially. However, you can do a [cred #### What will happen when I exceed the Free Plan quota? -You will be notified when you exceed the Free Plan quota. It is important to take action at this point. If you continue to exceed the limits without reducing your usage, service restrictions will apply. To avoid service restrictions, you have two options: reduce your usage or upgrade to a paid plan. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. +You will be notified when you exceed the Free Plan quota. It is important to take action at this point. If you continue to exceed the limits, service restrictions will apply. To avoid service restrictions, you can [manage your usage](/docs/guides/platform/manage-your-usage) or upgrade to a paid plan. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. #### What will happen when I exceed the Pro Plan quota and have the spend cap on? -You will be notified when you exceed your Pro Plan quota. To unblock yourself, you can toggle off your spend cap in your [organization's billing settings](/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without reducing your usage or turning off the spend cap, restrictions will apply. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. +You will be notified when you exceed your Pro Plan quota. To unblock yourself, you can toggle off your spend cap in your [organization's billing settings](/dashboard/org/_/billing) to pay for over-usage beyond the Pro plans limits. If you continue to exceed the limits without managing your usage or turning off the spend cap, restrictions will apply. Learn more about restrictions in the [Fair Use Policy](#fair-use-policy) section. #### How do I scale beyond the limits of my Pro Plan? diff --git a/apps/docs/content/guides/platform/custom-domains.mdx b/apps/docs/content/guides/platform/custom-domains.mdx index 441a7657a4d..d0076126fa8 100644 --- a/apps/docs/content/guides/platform/custom-domains.mdx +++ b/apps/docs/content/guides/platform/custom-domains.mdx @@ -53,7 +53,7 @@ You need to add a CNAME record to your domain's DNS settings to ensure your cust If your project's default domain is `abcdefghijklmnopqrst.supabase.co` you should: - Create a CNAME record for `api.example.com` that resolves to `abcdefghijklmnopqrst.supabase.co.`. -- Use a low TTL value to quickly propagate changes in case you make a mistake. +- Use a low TTL value to propagate changes in case you make a mistake. ### Verify ownership of the domain @@ -73,7 +73,7 @@ Required outstanding validation records: _acme-challenge.api.example.com. TXT -> ca3-F1HvR9i938OgVwpCFwi1jTsbhe1hvT0Ic3efPY3Q ``` -Add the record to your domains' DNS settings. Make sure to trim surrounding whitespace. Use a low TTL value so you can quickly change the records if you make a mistake. +Add the record to your domains' DNS settings. Make sure to trim surrounding whitespace. Use a low TTL value so you can change the records if you make a mistake. Some DNS registrars automatically append your domain name to the DNS entries being created. As such, creating a DNS record for `api.example.com` might instead create a record for `api.example.com.example.com`. In such cases, remove the domain name from the records you're creating; as an example, you would create a TXT record for `api`, instead of `api.example.com`. @@ -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,14 +161,14 @@ 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 Use the [`vanity-subdomains check-availability`](/docs/reference/cli/supabase-vanity-subdomains-check-availability) command of the CLI to check if your desired subdomain is available for use: ```bash -supabase vanity-subdomains --project-ref abcdefghijklmnopqrst check-availability --desired-subdomain my-example-brand --experimental +supabase vanity-subdomains check-availability --project-ref abcdefghijklmnopqrst --desired-subdomain my-example-brand --experimental ``` ### Prepare to activate the subdomain @@ -199,7 +200,7 @@ Once you've chosen an available subdomain and have done all the necessary prepar Use the [`vanity-subdomains activate`](/docs/reference/cli/supabase-vanity-subdomains-activate) command to activate and claim your subdomain: ```bash -supabase vanity-subdomains --project-ref abcdefghijklmnopqrst activate --desired-subdomain my-example-brand --experimental +supabase vanity-subdomains activate --project-ref abcdefghijklmnopqrst --desired-subdomain my-example-brand --experimental ``` If you wish to use the new domain in client code, you can set it up like so: diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index f2e63601fb7..bf241988418 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -114,6 +114,14 @@ Free Plan projects enter [read-only](#read-only-mode) mode when your **database - [Upgrade to the Pro Plan](/dashboard/org/_/billing) to increase the database size quota. [Disable the Spend Cap](https://app.supabase.com/org/_/billing?panel=costControl) if you want your Pro instance to auto-scale beyond the 8 GB disk size limit. - [Disable read-only mode](#disabling-read-only-mode) and reduce your database size. +### Fair use database size restriction + +Separate from the per-project read-only mode above, your organization can be placed under a [Fair Use](/docs/guides/platform/billing-faq#fair-use-policy) service restriction (requests return a `402` status code) when its database size exceeds the plan quota. This quota is evaluated **per organization**, summing the database size across all of your projects. + +Importantly, it is based on the **average daily database size over the billing period**, not the live size. Reducing your database size does not immediately lift the restriction: the average stays elevated until enough lower-usage days accumulate, and it effectively resets when your billing cycle rolls over. This is why a project that is well under the limit today can still be restricted, as its average across the period is still over. + +To resolve it, upgrade your plan or disable your Spend Cap to lift the restriction immediately. Otherwise, reduce your database size and wait for the new billing cycle, at which point the average restarts from your current size. + ### Read-only mode In some cases Supabase may put your database into read-only mode to prevent your database from exceeding the billing or disk limitations. diff --git a/apps/docs/content/guides/platform/delete-project.mdx b/apps/docs/content/guides/platform/delete-project.mdx new file mode 100644 index 00000000000..3c3214d62fd --- /dev/null +++ b/apps/docs/content/guides/platform/delete-project.mdx @@ -0,0 +1,136 @@ +--- +title: Deleting Your Project +subtitle: Understanding the permanent consequences and how to protect yourself +--- + +Deleting a Supabase project is a **permanent and irreversible action**. Before proceeding, understand the full scope of what will be deleted and take precautions to prevent accidental data loss. + + + +We cannot recover deleted projects. All data, backups, and configurations are permanently removed. Ensure you have exported all critical data or saved backups before proceeding to delete your project. + + + +## What gets deleted + +When you delete a project, **all artifacts are permanently removed and you cannot recover them**. +Some of these artifacts includes (but are not limited to): + +- **Database and all data**: Your entire Postgres database, including all tables, schemas, and records +- **Edge Functions**: All deployed Edge Functions and their source code are removed +- **Storage objects**: All files in Storage buckets are permanently deleted +- **Backups**: All automated backups and point-in-time recovery snapshots are inaccessible +- **Authentication data**: User accounts, sessions, and auth logs are removed +- **Real-time subscriptions**: All active subscriptions and configurations are cleared +- **API keys and credentials**: All project API keys, service role keys, and webhooks are invalidated +- **Custom domains and SSL certificates**: Any custom domains linked to the project are removed + +## How to delete a project + +You can delete a project through any of these methods: + +### Via dashboard + +1. Navigate to your project's [**Settings** > **General** > **Delete project**](/dashboard/project/qiuwhyoiycwkuobqirkr/settings/general#delete-project) +2. Click **Delete Project** +3. Enter your project name exactly as it appears to confirm +4. Review the confirmation dialog and click **Delete** + +### Via Supabase CLI + +```bash +supabase projects delete +``` + +For more information, see the [Supabase CLI documentation](/docs/reference/cli/supabase-projects-delete). + +### Via management API + +```bash +curl -X DELETE https://api.supabase.com/v1/projects/ \ + -H "Authorization: Bearer " +``` + +For more information, see the [Management API documentation](/docs/reference/api/v1-delete-a-project). + +## After deletion + +Once a project is deleted: + +- Your project URL will no longer be accessible +- DNS records will be cleaned up (may take up to 24 hours to fully propagate) +- Billing for this project will stop immediately +- You cannot recover or restore the project + + + +Deleting projects stops new usage from accumulating, but does not remove usage that already occurred during the current billing cycle. For quota-based limits, that usage still counts until the billing period resets. +See our [Fair Use Policy](/docs/guides/platform/billing-faq#fair-use-policy) for further details. + + + +## Alternative: Pause your project + +If you're unsure about deletion, consider pausing your project instead: + + + +Note: Only Free Projects can be paused at this time. + + + +- **Paused projects** stop incurring compute charges +- **Data is preserved** and can be accessed when you resume +- **Quick recovery** — Resume the project at any time without data loss + +To pause a project, navigate to [**Settings** > **General** > **Project availability**](/dashboard/project/qiuwhyoiycwkuobqirkr/settings/general) and click **Pause Project**. + +## Protective measures to consider + +To safeguard against accidental deletion, consider the following: + +### 1. Backup your database + +You can backup your database by following the [backup and restore guide](/docs/guides/platform/migrating-within-supabase/backup-restore) provided. + +If you are on the Pro, Team or Enterprise Plan, with legacy logical backups, you can download a copy from [your Supabase dashboard](/dashboard/project/_/database/backups/scheduled) + +### 2. Download storage objects + +- Back up all important files from Storage buckets +- Use the Supabase dashboard or API to download files in bulk +- Store in a secure location outside of Supabase + +### 3. Document configuration + +- Export Edge Function code and configurations from the dashboard +- Save authentication provider settings (OAuth, SAML, etc.) +- Document any custom database functions, triggers, or policies +- Record webhook configurations and integrations + +### 4. Enable access controls + + + +Restrict who can delete projects within your organization to prevent accidental deletion by team members. +See [Access Controls](/docs/guides/platform/access-control) + + + +- Set up role-based access controls to limit deletion permissions +- Require multi-factor authentication (MFA) for sensitive operations +- Use the Supabase dashboard to configure team member permissions + +### 5. Monitor project activity + +- Enable audit logging to track who has access to your project +- Review and revoke unnecessary API keys before deletion +- Check for any scheduled jobs or integrations that depend on the project + +## Need help? + +If you're unsure about deletion or need assistance: + +- Review this guide and the protection measures above +- Contact [Supabase support](/support) for guidance +- Consider reaching out to your team before deleting shared projects diff --git a/apps/docs/content/guides/platform/free-project-pausing.mdx b/apps/docs/content/guides/platform/free-project-pausing.mdx new file mode 100644 index 00000000000..9e6b2fc6bbf --- /dev/null +++ b/apps/docs/content/guides/platform/free-project-pausing.mdx @@ -0,0 +1,45 @@ +--- +title: 'Project Pausing' +description: 'Free project pausing behavior.' +--- + +{/* supa-mdx-lint-disable-next-line Rule003Spelling */} +Supabase pauses Free Plan projects that show low activity over a 7-day period to save server resources. This guide explains how pausing works, how to restore a paused project, and how to avoid pausing altogether. + + + +Projects under a paid plan cannot be paused and are not subject to automatic pausing for inactivity. To pause a project currently under a paid plan, first transfer the project to an organization on the Free plan. + + + +## How automatic pausing works + +A Free plan project is considered inactive if it does not receive sufficient user database activity over the past week. Projects with too few user queries during that window are the clearest candidates for pausing. While you may be actively using the project, it's possible that usage is not enough to exclude it from automatic pausing. Typically a few user requests to the database each day over the previous week is enough to keep the project from being paused. + +Supabase sends two emails to the project owner regarding project pausing: + +1. A warning email roughly one week before the pause takes effect. +2. A confirmation email once the project has been paused. + +After receiving an initial email warning, the pause can be prevented by taking one of the following steps: + +- Visit the project from the [Supabase Dashboard](/dashboard/project/_) to generate activity. +- Generate a sufficient amount of activity by making API calls to your project or sending requests via your connected application. + +## Restoring a paused project + +You can restore a paused project for up to 90 days after it was paused: + +1. Open the [Supabase Dashboard](/dashboard/organizations) +2. Select the organization, followed by the paused project +3. Click **Resume project** and confirm + +The project will return to its previous state, including data and configurations. + +### 90-day window to restore + +Once the project is paused, there is 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 backward compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes. + +## Preventing automatic project pausing + +To prevent future automatic pausing, upgrade to the Pro Plan from [Billing Settings](/dashboard/org/_/billing?panel=subscriptionPlan). Paid projects cannot be paused and are not subject to pausing for inactivity. diff --git a/apps/docs/content/guides/platform/hipaa-projects.mdx b/apps/docs/content/guides/platform/hipaa-projects.mdx index 2e91554e454..78f1864bce1 100644 --- a/apps/docs/content/guides/platform/hipaa-projects.mdx +++ b/apps/docs/content/guides/platform/hipaa-projects.mdx @@ -24,5 +24,6 @@ These include: - Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-add-ons). - Turning on [SSL Enforcement](/docs/guides/platform/ssl-enforcement). - Enabling [Network Restrictions](/docs/guides/platform/network-restrictions). +- Keeping [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) enabled. Additional security checks and controls will be added as the security advisor is extended and additional security controls are made available. diff --git a/apps/docs/content/guides/platform/ipv4-address.mdx b/apps/docs/content/guides/platform/ipv4-address.mdx index 7dc01f9765b..f17be34c3cd 100644 --- a/apps/docs/content/guides/platform/ipv4-address.mdx +++ b/apps/docs/content/guides/platform/ipv4-address.mdx @@ -77,7 +77,7 @@ By default, Supabase Postgres use IPv6 addresses. If your system doesn't support ### Checking your network IPv6 support -You can check if your personal network is IPv6 compatible at https://test-ipv6.com. +You can check if your personal network is IPv6 compatible at https://ipv6test.google.com/. ### Checking platforms for IPv6 support: diff --git a/apps/docs/content/guides/platform/manage-your-usage/branching.mdx b/apps/docs/content/guides/platform/manage-your-usage/branching.mdx index 774214977f3..64c6f18a4bd 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/branching.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/branching.mdx @@ -5,7 +5,7 @@ title: 'Manage Branching usage' ## What you are charged for -Each [Preview branch](/docs/guides/deployment/branching) is a separate environment with all Supabase services (Database, Auth, Storage, etc.). You're charged for usage within that environment—such as [Compute](/docs/guides/platform/manage-your-usage/compute), [Disk Size](/docs/guides/platform/manage-your-usage/disk-size), [Egress](/docs/guides/platform/manage-your-usage/egress), and [Storage](/docs/guides/platform/manage-your-usage/storage-size)—just like the project you branched from. +Each [Preview branch](/docs/guides/deployment/branching) is a separate environment with all Supabase services (Database, Auth, Storage, etc.). You're charged for usage within that environment—such as [Compute](/docs/guides/platform/manage-your-usage/compute), [Disk Size](/docs/guides/platform/manage-your-usage/disk-size), [Egress](/docs/guides/platform/manage-your-usage/egress), and [Storage](/docs/guides/platform/manage-your-usage/storage-size)—the same as the project you branched from. 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/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx index 4469a946e73..b05dbb00359 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx @@ -63,6 +63,12 @@ Cached and uncached egress have independent quotas and independent pricing. Cach Egress is charged by gigabyte. Charges apply only for usage exceeding your subscription plan's quota. This quota is called the Unified Egress Quota because it can be used across all services (Database, Auth, Storage etc.). + + +Egress accumulates over the billing cycle and resets at the start of the next cycle. Usage that has already been served cannot be reduced retroactively, so the optimizations below lower future egress only. If your organization is restricted for egress, the restriction clears at the start of the next billing cycle, or immediately if you upgrade your plan or disable your Spend Cap. + + + ### Usage on your invoice Usage is shown as "Egress GB" and "Cached Egress GB" on your invoice. diff --git a/apps/docs/content/guides/platform/manage-your-usage/ipv4.mdx b/apps/docs/content/guides/platform/manage-your-usage/ipv4.mdx index a43266ac178..8c004f63755 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/ipv4.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/ipv4.mdx @@ -117,4 +117,4 @@ If you remove the IPv4 add-on, you are no longer billed from the time of removal ## Optimize usage -To see whether your database actually needs a dedicated IPv4 address, refer to [When you need the IPv4 add-on](/docs/guides/platform/ipv4-address#when-you-need-the-ipv4-add-on). +To see whether your database needs a dedicated IPv4 address, refer to [When you need the IPv4 add-on](/docs/guides/platform/ipv4-address#when-you-need-the-ipv4-add-on). 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 index af40d0e9678..7c26bbc978c 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/logs-ingest.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/logs-ingest.mdx @@ -65,7 +65,7 @@ Every service in your Supabase project automatically generates Logs — you don' - **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. +- **Cap log payload size.** Large structured payloads can inflate GB-billed volume. - **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 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/manage-your-usage/storage-size.mdx b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx index eca3dd5544c..b2dfc468eff 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/storage-size.mdx @@ -12,6 +12,8 @@ You are charged for the total size of all assets in your buckets. Storage size is charged by Gigabyte-Hours (GB-Hrs). 1 GB-Hr represents the use of 1 GB of storage for 1 hour. For example, storing 10 GB of data for 5 hours results in 50 GB-Hrs (10 GB × 5 hours). +Because usage is measured in GB-Hrs, your Storage size for quota and billing is effectively the average across the billing period, not the live size. For example, storing 20 GB for the first half of the month and 0 GB for the second half averages to 10 GB. This means reducing storage late in the cycle lowers the average only gradually, so it may not immediately clear a restriction until the next billing cycle begins. + ### Usage on your invoice Usage is shown as "Storage Size GB-Hrs" on your invoice. diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/auth0.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/auth0.mdx index c8f9825d7ef..806660c54a5 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/auth0.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/auth0.mdx @@ -80,6 +80,7 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch ```ts import { createClient } from '@supabase/supabase-js' + const supabase = createClient('your_project_url', 'your_supabase_api_key') // ---cut--- @@ -100,6 +101,7 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch ```ts import { createClient } from '@supabase/supabase-js' + const supabase = createClient('your_project_url', 'your_supabase_api_key') // ---cut--- @@ -122,6 +124,7 @@ For passwordless signin via email or phone, check for users with verified email ```ts import { createClient } from '@supabase/supabase-js' + const supabase = createClient('your_project_url', 'your_supabase_api_key') // ---cut--- @@ -162,6 +165,7 @@ Both columns are accessible from the admin user methods. To create a user with c ```ts import { createClient } from '@supabase/supabase-js' + const supabase = createClient('your_project_url', 'your_supabase_api_key') // ---cut--- @@ -198,7 +202,6 @@ create table private.user_metadata ( I have IDs assigned to existing users in my database, how can I maintain these IDs?} +header="I have IDs assigned to existing users in my database, how can I maintain these IDs?" id="custom-user-id" > @@ -229,7 +232,7 @@ const { data, error } = await supabase.auth.admin.createUser({ How can I allow my users to retain their existing password?} +header="How can I allow my users to retain their existing password?" id="existing-password" > @@ -238,7 +241,7 @@ Supabase Auth never stores passwords as plaintext. Since Supabase Auth supports My users have multi-factor authentication (MFA) enabled, how do I make sure they don't have to set up MFA again?} +header="My users have multi-factor authentication (MFA) enabled, how do I make sure they don't have to set up MFA again?" id="mfa" > @@ -247,7 +250,7 @@ You can obtain an export of your users' MFA secrets by opening a support ticket How do I migrate existing SAML Single Sign-On (SSO) connections?} +header="How do I migrate existing SAML Single Sign-On (SSO) connections?" id="saml" > @@ -256,7 +259,7 @@ Customers may need to link their identity provider with Supabase Auth separately How do I migrate my Auth0 organizations to Supabase?} +header="How do I migrate my Auth0 organizations to Supabase?" id="migrate-org" > diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx index de95b7aee8b..aafd172a0b4 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx @@ -124,7 +124,7 @@ module.exports = (collectionName, doc, recordCounters, writeRecord) => { - `writeRecord`: This function automatically handles the process of writing data to other JSON files (useful for "flatting" your document into separate JSON files to be written to separate database tables). `writeRecord` takes the following parameters: - `name`: Name of the JSON file to write to. - `doc`: The document to write to the file. - - `recordCounters`: The same `recordCounters` object that was passed to this hook (just passes it on). + - `recordCounters`: The same `recordCounters` object that was passed to this hook (passes it on). ### Examples diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx index f361f224f8d..036416e8746 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/heroku.mdx @@ -8,7 +8,7 @@ tocVideo: 'xsRhPMphtZ4' Supabase is one of the best [free alternatives to Heroku Postgres](/alternatives/supabase-vs-heroku-postgres). This guide shows how to migrate your Heroku Postgres database to Supabase. This migration requires the [pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) and [psql](https://www.postgresql.org/docs/current/app-psql.html) CLI tools, which are installed automatically as part of the complete Postgres installation package. -Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supabase.com/) to migrate in just a few clicks. +Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supabase.com/) to migrate in a few clicks. ## Quick demo 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..cd989bbccc6 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,24 +74,21 @@ breadcrumb: 'Migrations' -## Before you begin +### Before you begin -
- - <$Partial path="postgres_installation.mdx" /> - -
+ + <$Partial path="postgres_installation.mdx" /> +
-## Restore backup using CLI +### Restore backup using CLI @@ -197,9 +194,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 +211,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 +220,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 +234,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 +245,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 +261,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 +276,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 +323,7 @@ DROP ROLE IF EXISTS cli_login_postgres; -## Steps (using the Supabase Dashboard): +### Steps (using the Supabase Dashboard): @@ -369,7 +366,7 @@ Dependencies defined through [import maps](/docs/guides/functions/dependencies#u -# Migrating storage objects +## Migrating storage objects @@ -808,6 +805,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 3e59cdeb9a7..0da67357ab3 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 @@ -14,23 +14,18 @@ Dashboard backups are only available for older projects that still use logical b -
- <$Partial path="postgres_installation.mdx" /> - -
-
- @@ -52,7 +47,6 @@ Dashboard backups are only available for older projects that still use logical b -
## Things to keep in mind diff --git a/apps/docs/content/guides/platform/network-restrictions.mdx b/apps/docs/content/guides/platform/network-restrictions.mdx index 9785743fb4b..b91a0ddc290 100644 --- a/apps/docs/content/guides/platform/network-restrictions.mdx +++ b/apps/docs/content/guides/platform/network-restrictions.mdx @@ -4,99 +4,102 @@ title: 'Network Restrictions' description: "Apply network restrictions for your project's database." --- +This topic explains how to configure network restrictions for your Supabase project's database. Network restrictions let you control which IP ranges can connect to Postgres and its pooler, reducing your project's exposure to unauthorized access. + -If you can't find the Network Restrictions section at the bottom of your [Database Settings](/dashboard/project/_/database/settings), update your version of Postgres in the [Infrastructure Settings](/dashboard/project/_/settings/infrastructure). +If you can't find the Network Restrictions section in your [Database Settings](/dashboard/project/_/database/settings), update your Postgres version in [Infrastructure Settings](/dashboard/project/_/settings/infrastructure). -Each Supabase project comes with configurable restrictions on the IP ranges that are allowed to connect to Postgres and its pooler ("your database"). These restrictions are enforced before traffic reaches your database. If a connection is not restricted by IP, it still needs to authenticate successfully with valid database credentials. +Each Supabase project supports configurable restrictions on the IP ranges allowed to connect to Postgres and its pooler. These restrictions are enforced before traffic reaches your database. Connections that aren't restricted by IP still need to authenticate with valid database credentials. -If direct connections to your database [resolve to a IPv6 address](/dashboard/project/_/database/settings), you need to add both IPv4 and IPv6 CIDRs to the list of allowed CIDRs. Network Restrictions will be applied to all database connection routes, whether pooled or direct. You will need to add both the IPv4 and IPv6 networks you want to allow. There are two exceptions: If you have been granted an extension on the IPv6 migration OR if you have purchased the [IPv4 add-on](/dashboard/project/_/settings/addons), you need only add IPv4 CIDRs. +If direct connections to your database [resolve to an IPv6 address](/dashboard/project/_/database/settings), add both IPv4 and IPv6 CIDRs to your allowlist. Network restrictions apply to all connection routes, whether pooled or direct. There are two exceptions: if you have an extension on the IPv6 migration, or if you have the [IPv4 add-on](/dashboard/project/_/settings/addons), you only need to add IPv4 CIDRs. -## To get started via the Dashboard: +## Configure with the dashboard [#to-get-started-via-the-dashboard] -Network restrictions can be configured in the [Database Settings](/dashboard/project/_/database/settings) page. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling network restrictions. +To configure network restrictions with the dashboard: -## To get started via the Management API: +1. Open your project's [Database Settings](/dashboard/project/_/database/settings) page. +1. In the Network Restrictions section, make your changes. You need [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) to make changes. -You can also manage network restrictions using the Management API: +## Configure with the CLI [#to-get-started-via-the-cli] -```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" - -# Get current network restrictions -curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/network-restrictions" \ - -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" - -# Update network restrictions -curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/network-restrictions/apply" \ - -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "db_allowed_cidrs": [ - "192.168.0.1/24", - ] - }' -``` - -## To get started via the CLI: +To configure network restrictions with the CLI: 1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+. -1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI. -1. If your project was created before 23rd December 2022, it will need to be [upgraded to the latest Supabase version](/docs/guides/platform/migrating-and-upgrading-projects) before Network Restrictions can be used. -1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling network restrictions. +1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account. +1. If your project was created before December 23, 2022, [upgrade it to the latest Supabase version](/docs/guides/platform/migrating-and-upgrading-projects) before using network restrictions. +1. Ensure you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. ### Check restrictions -You can use the `get` subcommand of the CLI to retrieve the restrictions currently in effect. +To check your current network restrictions: -If restrictions have been applied, the output of the `get` command will reflect the IP ranges allowed to connect: +1. Complete the steps in [Configure with the CLI](#to-get-started-via-the-cli). +1. Run the `get` subcommand to retrieve the restrictions currently in effect: -```bash -> supabase network-restrictions --project-ref {ref} get --experimental -DB Allowed IPv4 CIDRs: &[183.12.1.1/24] -DB Allowed IPv6 CIDRs: &[2001:db8:3333:4444:5555:6666:7777:8888/64] -Restrictions applied successfully: true -``` + ```bash + > supabase network-restrictions get --project-ref {ref} --experimental + DB Allowed IPv4 CIDRs: &[183.12.1.1/24] + DB Allowed IPv6 CIDRs: &[2001:db8:3333:4444:5555:6666:7777:8888/64] + Restrictions applied successfully: true + ``` -If restrictions have never been applied to your project, the list of allowed CIDRs will be empty, but they will also not have been applied ("Restrictions applied successfully: false"). As a result, all IPs are allowed to connect to your database: + If restrictions have never been applied, the allowed CIDRs list is empty and `Restrictions applied successfully` is `false`. All IPs can connect: -```bash -> supabase network-restrictions --project-ref {ref} get --experimental -DB Allowed IPv4 CIDRs: [] -DB Allowed IPv6 CIDRs: [] -Restrictions applied successfully: false -``` + ```bash + > supabase network-restrictions get --project-ref {ref} --experimental + DB Allowed IPv4 CIDRs: [] + DB Allowed IPv6 CIDRs: [] + Restrictions applied successfully: false + ``` ### Update restrictions -The `update` subcommand is used to apply network restrictions to your project: +To update your network restrictions: -```bash -> supabase network-restrictions --project-ref {ref} update --db-allow-cidr 183.12.1.1/24 --db-allow-cidr 2001:db8:3333:4444:5555:6666:7777:8888/64 --experimental -DB Allowed IPv4 CIDRs: &[183.12.1.1/24] -DB Allowed IPv6 CIDRs: &[2001:db8:3333:4444:5555:6666:7777:8888/64] -Restrictions applied successfully: true -``` +1. Complete the steps in [Configure with the CLI](#to-get-started-via-the-cli). +1. Run the `update` subcommand with the CIDRs you want to allow: -The restrictions specified (in the form of CIDRs) replaces any restrictions that might have been applied in the past. -To add to the existing restrictions, you must include the existing restrictions within the list of CIDRs provided to the `update` command. + ```bash + > supabase network-restrictions update --project-ref {ref} --db-allow-cidr 183.12.1.1/24 --db-allow-cidr 2001:db8:3333:4444:5555:6666:7777:8888/64 --experimental + DB Allowed IPv4 CIDRs: &[183.12.1.1/24] + DB Allowed IPv6 CIDRs: &[2001:db8:3333:4444:5555:6666:7777:8888/64] + Restrictions applied successfully: true + ``` + + The CIDRs you provide replace any previously applied restrictions. To keep existing restrictions, include them alongside any new CIDRs in the `update` command. + +### Append a CIDR to existing restrictions + +To append a CIDR to your existing restrictions: + +1. Complete the steps in [Configure with the CLI](#to-get-started-via-the-cli). +1. Run the `update` subcommand with the `--append` flag to add a CIDR without replacing existing restrictions: + + ```bash + > supabase network-restrictions update --project-ref {ref} --db-allow-cidr 1.2.3.4/32 --append --experimental + DB Allowed IPv4 CIDRs: &[183.12.1.1/24 1.2.3.4/32] + DB Allowed IPv6 CIDRs: &[2001:db8:3333:4444:5555:6666:7777:8888/64] + Restrictions applied successfully: true + ``` ### Remove restrictions -To remove all restrictions on your project, you can use the `update` subcommand with the CIDR `0.0.0.0/0`: +To remove all network restrictions: -```bash -> supabase network-restrictions --project-ref {ref} update --db-allow-cidr 0.0.0.0/0 --db-allow-cidr ::/0 --experimental -DB Allowed IPv4 CIDRs: &[0.0.0.0/0] -DB Allowed IPv6 CIDRs: &[::/0] -Restrictions applied successfully: true -``` +1. Complete the steps in [Configure with the CLI](#to-get-started-via-the-cli). +1. Run the `update` subcommand with the CIDR `0.0.0.0/0` to remove all restrictions: + + ```bash + > supabase network-restrictions update --project-ref {ref} --db-allow-cidr 0.0.0.0/0 --db-allow-cidr ::/0 --experimental + DB Allowed IPv4 CIDRs: &[0.0.0.0/0] + DB Allowed IPv6 CIDRs: &[::/0] + Restrictions applied successfully: true + ``` ## Limitations -- The current iteration of Network Restrictions applies to connections to Postgres and the database pooler; it doesn't currently apply to APIs offered over HTTPS (e.g., PostgREST, Storage, and Auth). This includes using Supabase client libraries like [supabase-js](/docs/reference/javascript). -- If network restrictions are enabled, direct access to your database from Edge Functions will always be blocked. Using the Supabase client library [supabase-js](/docs/reference/javascript) is recommended to connect to a database with network restrictions from Edge Functions. +- Network restrictions apply to Postgres and the database pooler. They don't apply to HTTPS APIs such as PostgREST, Storage, and Auth, or to Supabase client libraries like [supabase-js](/docs/reference/javascript). +- With network restrictions applied, Edge functions lose direct access to the database. Use [supabase-js](/docs/reference/javascript) to connect to the database from Edge Functions instead. diff --git a/apps/docs/content/guides/platform/performance.mdx b/apps/docs/content/guides/platform/performance.mdx index 1bc78848a4c..b95198471ff 100644 --- a/apps/docs/content/guides/platform/performance.mdx +++ b/apps/docs/content/guides/platform/performance.mdx @@ -4,7 +4,7 @@ title: 'Performance Tuning' description: 'Getting the best results out of your Supabase project' --- -The Supabase platform automatically optimizes your Postgres database to take advantage of the compute resources of the plan your project is on. However, these optimizations are based on assumptions about the type of workflow the project is being utilized for, and it is likely that better results can be obtained by tuning the database for your particular workflow. +The Supabase platform automatically optimizes your Postgres database to take advantage of the compute resources of the plan your project is on. However, these optimizations are based on assumptions about the type of workflow the project is being used for, and it is likely that better results can be obtained by tuning the database for your particular workflow. ## Examining query performance @@ -31,7 +31,7 @@ In such a scenario, you can consider: You can use the [pg_stat_activity](https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW) view to debug which clients are holding open connections on your DB. `pg_stat_activity` only exposes information on direct connections to the database. Information on the number of connections to Supavisor is available [via the metrics endpoint](../telemetry/metrics). -Depending on the clients involved, you might be able to configure them to work with fewer connections (e.g. by imposing a limit on the maximum number of connections they're allowed to use), or shift specific workloads to connect via [Supavisor](/docs/guides/database/connecting-to-postgres#connection-pooler) instead. Transient workflows, which can quickly scale up and down in response to traffic (e.g. serverless functions), can especially benefit from using a connection pooler rather than connecting to the DB directly. +Depending on the clients involved, you might be able to configure them to work with fewer connections (e.g. by imposing a limit on the maximum number of connections they're allowed to use), or shift specific workloads to connect via [Supavisor](/docs/guides/database/connecting-to-postgres#connection-pooler) instead. Transient workflows, which can scale up and down rapidly in response to traffic (e.g. serverless functions), can especially benefit from using a connection pooler rather than connecting to the DB directly. ### Allowing higher number of connections diff --git a/apps/docs/content/guides/platform/postgres-connection-logging.mdx b/apps/docs/content/guides/platform/postgres-connection-logging.mdx new file mode 100644 index 00000000000..866336b8046 --- /dev/null +++ b/apps/docs/content/guides/platform/postgres-connection-logging.mdx @@ -0,0 +1,79 @@ +--- +id: 'postgres-connection-logging' +title: 'Postgres connection logging' +description: 'Enable or disable Postgres connection logging for audit and compliance.' +--- + +For security monitoring and compliance audits, Postgres can log connection lifecycle events to your project's [Postgres logs](/docs/guides/telemetry/logs#postgres), including events such as `connection received`, `connection authenticated`, and `connection authorized`. + +## Default behavior + +By default, Supabase sets `log_connections` to off for new projects and you must enable it first. This behavior matches common managed Postgres defaults and reduces log volume from high-frequency connection events. + +Existing projects may retain different settings depending on plan and compliance configuration: + +- **Team, Enterprise, and HIPAA organizations** — Connection logging is typically enabled to support audit requirements. +- **HIPAA projects** — Supabase enables connection logging when a project is marked as high compliance. The [Security Advisor](/dashboard/project/_/advisors/security) warns if connection logging is later disabled. + +## Compliance considerations + + + +If you need connection audit evidence for SOC 2 or other compliance programs, you must enable it explicitly. + + + +Connection logging supports audit and monitoring controls required by some compliance programs: + +- **HIPAA** — High-compliance projects should keep connection logging enabled. See the [shared responsibility model for healthcare data](/docs/guides/deployment/shared-responsibility-model#managing-healthcare-data) and [HIPAA compliance guide](/docs/guides/security/hipaa-compliance). +- **SOC 2** — Users who need connection audit evidence should enable logging and retain logs according to their own policies. See the [SOC 2 compliance guide](/docs/guides/security/soc-2-compliance). + +Disabling connection logging does not affect other Supabase logging (for example, [Platform Audit Logs](/docs/guides/security/platform-audit-logs), [Auth Audit Logs](/docs/guides/auth/audit-logs), or [pgAudit](/docs/guides/telemetry/logs#configuring-pgauditlog)). + +## Manage connection logging via the dashboard + +You can configure connection logging from the **Log connections** setting in the [Database Settings](/dashboard/project/_/database/settings) section of the Dashboard. + +Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. + + + +Connection events appear in Postgres logs. In the [Logs Explorer](/dashboard/project/_/logs-explorer), connection lifecycle messages may be hidden by default to reduce noise. Use the connection logs filter in the sidebar to show or hide them. + + + +## Manage connection logging via the Management API + +You can also manage connection logging using the [Management API](/docs/reference/api/v1-update-postgres-config): + +```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" + +# Get current Postgres config +curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/database/postgres" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" + +# Enable connection logging +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/config/database/postgres" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "log_connections": true + }' + +# Disable connection logging +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/config/database/postgres" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "log_connections": false + }' +``` + +To verify the setting, use the SQL Editor: + +```sql +show log_connections; +``` diff --git a/apps/docs/content/guides/platform/privatelink.mdx b/apps/docs/content/guides/platform/privatelink.mdx index 4e602ed56cc..d3417eb68a5 100644 --- a/apps/docs/content/guides/platform/privatelink.mdx +++ b/apps/docs/content/guides/platform/privatelink.mdx @@ -26,7 +26,7 @@ Supabase PrivateLink is an organisation level configuration. It works by sharing The connection architecture changes from public internet routing to a dedicated private path through AWS's secure network backbone. -Supabase PrivateLink is currently just for direct database and PgBouncer connections only. It does not support other Supabase services like API, Storage, Auth, or Realtime. These services will continue to operate over public internet connections. +Supabase PrivateLink currently supports direct database and PgBouncer connections only. It does not support other Supabase services like API, Storage, Auth, or Realtime. These services will continue to operate over public internet connections. ## Requirements diff --git a/apps/docs/content/guides/platform/read-replicas.mdx b/apps/docs/content/guides/platform/read-replicas.mdx index 2fb83ea8c4f..b709c7281d9 100644 --- a/apps/docs/content/guides/platform/read-replicas.mdx +++ b/apps/docs/content/guides/platform/read-replicas.mdx @@ -32,29 +32,38 @@ You can only read data from a Read Replica. This is in contrast to a Primary dat -
- - When your database starts slowing down, you face a choice: make your existing database bigger (scale vertically), or spread the load across multiple databases (scale horizontally). Both approaches work. Neither is universally correct. The right answer depends on your workload, your budget, and where the bottleneck actually is. + When your database starts slowing down, you face a choice: make your existing database bigger (scale vertically), or spread the load across multiple databases (scale horizontally). Both approaches work. Neither is universally correct. The right answer depends on your workload, your budget, and where the bottleneck is. - Read Replicas decision flowchart + ```mermaid + flowchart TD + A[Database slowing down] --> B{CPU above 70% sustained?} + B -->|No| C[Monitor, do not scale yet] + B -->|Yes| D{Queries optimized? Indexes in place?} + D -->|No| E[Run EXPLAIN ANALYZE
Add missing indexes
Optimize first] + E --> D + D -->|Yes| F{Workload 80%+ reads?} + F -->|No| G[Upgrade compute
Replicas will not help writes] + F -->|Yes| H{Already at 16XL?} + H -->|Yes| I[Read Replicas
Only horizontal option left] + H -->|No| J{Need workload isolation
or geo-distribution?} + J -->|Yes| K[Read Replicas] + J -->|No| L[Either works
Compute is simpler
Replicas scale further] + ``` + + Supabase recommends not scaling until CPU is sustained above 70%. After it is, confirm your queries are already optimized and properly indexed with `EXPLAIN ANALYZE` before adding hardware. If your workload is less than roughly 80% reads, upgrade compute. Read Replicas only serve reads and won't help writes. If it's read-heavy, Read Replicas become the right choice after you reach the largest compute size of 16XL or earlier if you need workload isolation or geographic distribution. Below 16XL with no isolation need, either option works. While compute is simpler, Read Replicas scale further.
-
-
## Features diff --git a/apps/docs/content/guides/platform/read-replicas/getting-started.mdx b/apps/docs/content/guides/platform/read-replicas/getting-started.mdx index c4f768b47d5..085cdb2e218 100644 --- a/apps/docs/content/guides/platform/read-replicas/getting-started.mdx +++ b/apps/docs/content/guides/platform/read-replicas/getting-started.mdx @@ -100,7 +100,7 @@ We combine it with streaming replication to reduce replication lag. Once WAL-G f ### Restart or compute add-on change behaviour -When you restart a project that utilizes Read Replicas, or change the compute add-on size, the Primary database gets restarted first. During this period, the Read Replicas remain available. +When you restart a project that uses Read Replicas, or change the compute add-on size, the Primary database gets restarted first. During this period, the Read Replicas remain available. Once the Primary database has completed restarting (or resizing, in case of a compute add-on change) and become available for usage, all the Read Replicas are restarted (and resized, if needed) concurrently. @@ -138,7 +138,7 @@ If you are already ingesting your [project's metrics](/docs/guides/telemetry/met 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/ssl-enforcement.mdx b/apps/docs/content/guides/platform/ssl-enforcement.mdx index 74249a6da89..2f70acddc3f 100644 --- a/apps/docs/content/guides/platform/ssl-enforcement.mdx +++ b/apps/docs/content/guides/platform/ssl-enforcement.mdx @@ -70,7 +70,7 @@ To get started: You can use the `get` subcommand of the CLI to check whether SSL is currently being enforced: ```bash -supabase ssl-enforcement --project-ref {ref} get --experimental +supabase ssl-enforcement get --project-ref {ref} --experimental ``` Response if SSL is being enforced: @@ -90,13 +90,13 @@ SSL is *NOT* being enforced. The `update` subcommand is used to change the SSL enforcement status for your project: ```bash -supabase ssl-enforcement --project-ref {ref} update --enable-db-ssl-enforcement --experimental +supabase ssl-enforcement update --project-ref {ref} --enable-db-ssl-enforcement --experimental ``` Similarly, to disable SSL enforcement: ```bash -supabase ssl-enforcement --project-ref {ref} update --disable-db-ssl-enforcement --experimental +supabase ssl-enforcement update --project-ref {ref} --disable-db-ssl-enforcement --experimental ``` ### A note about Postgres SSL modes diff --git a/apps/docs/content/guides/platform/sso.mdx b/apps/docs/content/guides/platform/sso.mdx index 9a690a703e7..2349a28d27c 100644 --- a/apps/docs/content/guides/platform/sso.mdx +++ b/apps/docs/content/guides/platform/sso.mdx @@ -74,7 +74,7 @@ Users start their login at supabase.com by entering their email address, then ar - **Login flows** - Choose between IdP-initiated (users start from identity provider), SP-initiated (users start at supabase.com), or both. IdP-initiated is recommended for most organizations and requires no domain configuration. See [Choosing the Right Login Flow](/docs/guides/platform/sso/choosing-login-flow) for guidance. - **Email domains** - Required only if you enable SP-initiated login. You can associate one or more email domains with your SSO provider. Users with matching email addresses can sign in via SSO at supabase.com. Not required for IdP-initiated flow. -- **Auto-join** - Optionally allow users with a matching domain to be added to your organization automatically when they sign in via SSO. Auto-join applies on every login, not just first signup, making it easy to test before enabling. +- **Auto-join** - Optionally allow users with a matching domain to join your organization automatically when they sign in via SSO. This applies on every login, not only on first signup. - **Default role for auto-joined users** - Choose the role (e.g., `Read-only`, `Developer`, `Administrator`, `Owner`) that automatically joined users receive. We recommend using `Developer` as the default (principle of least privilege) and promoting users individually as needed. Refer to [access control](/docs/guides/platform/access-control) for more information about roles. - **Invitation types** - When inviting users to your organization, you can explicitly choose whether the invitation requires SSO authentication or allows non-SSO login (password/social). This enables mixed authentication organizations with both SSO and non-SSO users. diff --git a/apps/docs/content/guides/platform/sso/azure.mdx b/apps/docs/content/guides/platform/sso/azure.mdx index 62d0a7ef3eb..6daa75a6f94 100644 --- a/apps/docs/content/guides/platform/sso/azure.mdx +++ b/apps/docs/content/guides/platform/sso/azure.mdx @@ -64,7 +64,7 @@ First you need to download Supabase's SAML metadata file. Click the button below Alternatively, visit this page to initiate a download: `https://alt.supabase.io/auth/v1/sso/saml/metadata?download=true` -Click on the _Upload metadata file_ option in the toolbar and select the file you just downloaded. +Click on the _Upload metadata file_ option in the toolbar and select the file you downloaded. ![Azure AD console: Supabase application, SAML-based Sign-on screen, selected Upload metadata file button](/docs/img/sso-azure-step-06-1.png) @@ -134,7 +134,7 @@ By default this setting is disabled, users logging in via SSO will not be added ![Auto-join disabled](/docs/img/sso-dashboard-configure-autojoin-disabled.png) -Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. Auto-join applies on **every login**, not just first signup - this makes it safe to test SSO before enabling this feature. +Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. Auto-join applies on **every login**, not only on first signup - this makes it safe to test SSO before enabling this feature. ![Auto-join enable](/docs/img/sso-dashboard-configure-autojoin-enabled.png) diff --git a/apps/docs/content/guides/platform/sso/gsuite.mdx b/apps/docs/content/guides/platform/sso/gsuite.mdx index 4babba24020..608d8f03d0c 100644 --- a/apps/docs/content/guides/platform/sso/gsuite.mdx +++ b/apps/docs/content/guides/platform/sso/gsuite.mdx @@ -144,7 +144,7 @@ By default this setting is disabled, users logging in via SSO will not be added ![Auto-join disabled](/docs/img/sso-dashboard-configure-autojoin-disabled.png) -Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. Auto-join applies on **every login**, not just first signup - this makes it safe to test SSO before enabling this feature. +Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. Auto-join applies on **every login**, not only on first signup - this makes it safe to test SSO before enabling this feature. ![Auto-join enable](/docs/img/sso-dashboard-configure-autojoin-enabled.png) diff --git a/apps/docs/content/guides/platform/sso/okta.mdx b/apps/docs/content/guides/platform/sso/okta.mdx index 370b410872a..b62cd6588ab 100644 --- a/apps/docs/content/guides/platform/sso/okta.mdx +++ b/apps/docs/content/guides/platform/sso/okta.mdx @@ -130,7 +130,7 @@ By default this setting is disabled, users logging in via SSO will not be added ![Auto-join disabled](/docs/img/sso-dashboard-configure-autojoin-disabled.png) -Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. Auto-join applies on **every login**, not just first signup - this makes it safe to test SSO before enabling this feature. +Toggle this on if you want SSO-authenticated users to be **automatically added to your organization** when they log in via SSO. Auto-join applies on **every login**, not only on first signup - this makes it safe to test SSO before enabling this feature. ![Auto-join enable](/docs/img/sso-dashboard-configure-autojoin-enabled.png) diff --git a/apps/docs/content/guides/platform/sso/testing-best-practices.mdx b/apps/docs/content/guides/platform/sso/testing-best-practices.mdx index f9c4a5de033..a85c237a688 100644 --- a/apps/docs/content/guides/platform/sso/testing-best-practices.mdx +++ b/apps/docs/content/guides/platform/sso/testing-best-practices.mdx @@ -180,7 +180,7 @@ Test with 2-3 additional users to verify: -**Recent improvement:** Auto-join now applies on EVERY login, not just first signup. This resolves a common issue where org owners would test with auto-join disabled, enable it, then log in again expecting to auto-join. +**Recent improvement:** Auto-join now applies on EVERY login, not only on first signup. This resolves a common issue where org owners would test with auto-join disabled, enable it, then log in again expecting to auto-join. @@ -253,7 +253,7 @@ Test with 2-3 additional users to verify: - Auto-join works when enabled - Users receive correct default role - Non-matching domains are excluded (if using SP-initiated with domains) -- Existing users auto-join on their next login (not just new signups) +- Existing users auto-join on their next login (not only on new signups) - Auto-join can be disabled and re-enabled as needed - Auto-join is idempotent (no duplicate memberships) - Auto-join works with IdP-initiated only (no domains) @@ -369,7 +369,7 @@ SSO accounts have specific restrictions to prevent accidental organization locko - SSO accounts CAN disable SSO providers - Non-SSO owners CAN delete SSO providers - Error messages clearly explain the restriction -- Restriction applies to all SSO accounts (not just certain roles) +- Restriction applies to all SSO accounts (not only certain roles) ## Common issues and troubleshooting @@ -387,7 +387,7 @@ Based on customer pain points that previously required support intervention: **Solution:** -- Auto-join now applies on **every login**, not just first signup +- Auto-join now applies on **every login**, not only on first signup - To test: Enable auto-join, log out completely, log back in via SSO - If still not working, verify domain configuration matches user email exactly @@ -795,7 +795,7 @@ Before rolling out SSO to your organization: - Auto-join adds users to correct organization (if enabled) - Auto-joined users receive correct default role -- Auto-join works on first login (not just signup) +- Auto-join works on first login (not only on signup) - Existing users auto-join when feature enabled - Auto-join is idempotent (no duplicate memberships) - Auto-join works with IdP-initiated (no domains required) diff --git a/apps/docs/content/guides/platform/temporary-access.mdx b/apps/docs/content/guides/platform/temporary-access.mdx index 32e067c380c..06f76a8bc15 100644 --- a/apps/docs/content/guides/platform/temporary-access.mdx +++ b/apps/docs/content/guides/platform/temporary-access.mdx @@ -97,6 +97,12 @@ curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit" \ ## Using temporary access + + +This feature does not work with IPv6 Transaction pooler (PgBouncer). Direct connections and connections through the IPv4 connection pooler are fully supported. + + + 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: @@ -111,8 +117,8 @@ Connecting via the shared connection pooler requires the addition of a new conne ``` # 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' +psql 'postgres://postgres.{project-ref}:sbp_111222333aaabbbccc@aws-1-us-west-1.pooler.supabase.com:5432/postgres?options=-c%20jit%3dtrue' # or as a connection info string -psql "host=aws-1-us-west-1.pooler.supabase.com user=postgres.{project-ref} options='-c jit=on'" +psql "host=aws-1-us-west-1.pooler.supabase.com user=postgres.{project-ref} options='-c jit=true'" ``` diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 58a2c79a6e1..d78437d7917 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. @@ -206,3 +189,83 @@ select graphql.resolve('{ __schema { queryType { name } } }'); ``` Existing projects on pg_graphql 1.5.x are not impacted unless they choose to upgrade. + +### Ltree indexes require reindexing after upgrade + +_Applies when upgrading to Postgres 15.18 or 17.10._ + + + +You are affected only if you have indexes on `ltree` columns and your database uses a multibyte encoding or a non-`libc` collation provider. + + + +After upgrading, indexes on `ltree` columns that were built under the previous version can return incomplete results until the index is rebuilt. For example, label searches silently miss rows that are present. This affects databases using a multibyte encoding, such as UTF-8, or a non-`libc` collation provider such as ICU or builtin. + +To mitigate this issue: + +1. Check whether your database needs reindexing: + + ```sql + select + pg_encoding_to_char(encoding) as encoding, + pg_encoding_max_length(encoding) as max_bytes_per_char, -- 1 = single-byte, >1 = multibyte + datlocprovider as collation_provider, -- 'c' libc, 'i' icu, 'b' builtin + (pg_encoding_max_length(encoding) > 1 or datlocprovider != 'c') as reindex_required + from pg_database + where datname = current_database(); + ``` + + If `reindex_required` is `false`, such as a single-byte encoding like LATIN1 with `libc` collation, no action is needed. + +2. If `reindex_required` is `true`, find the affected indexes: + + ```sql + select schemaname, tablename, indexname + from pg_indexes + where + indexname in ( + select c.relname + from + pg_index as i + join pg_class as c on i.indexrelid = c.oid + join pg_attribute as a on a.attrelid = i.indrelid and a.attnum = ANY(i.indkey) + join pg_type as t on a.atttypid = t.oid + where t.typname in ('ltree', '_ltree') + ); + ``` + +3. Reindex each affected index. `REINDEX INDEX CONCURRENTLY` runs online with no downtime: + + ```sql + REINDEX INDEX CONCURRENTLY ; + ``` + +### Custom operator selectivity estimators + +_Applies when upgrading to Postgres 15.18 or 17.10._ + +Attaching a non-built-in (extension- or user-provided) selectivity estimator function to an operator now requires superuser. Existing operators continue to work — the check only fires when an operator is (re)created, most commonly during `pg_dump` / `pg_restore`, a logical restore, or a branch. + +Because Supabase database roles are not superusers, recreating such an operator on your behalf (for example during a restore or branch) can fail with: + +``` +ERROR: must be superuser to specify a non-built-in restriction estimator function +``` + +Most projects are not affected. To check whether your database has any user-defined operators that reference a non-built-in estimator: + +```sql +SELECT n.nspname AS schema, o.oprname AS operator +FROM pg_operator o +JOIN pg_namespace n ON o.oprnamespace = n.oid +WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') + AND ((o.oprrest <> 0 AND o.oprrest::oid >= 10000) + OR (o.oprjoin <> 0 AND o.oprjoin::oid >= 10000)) + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_operator'::regclass AND d.objid = o.oid AND d.deptype = 'e' + ); +``` + +If this returns no rows, your project is unaffected. diff --git a/apps/docs/content/guides/queues/pgmq.mdx b/apps/docs/content/guides/queues/pgmq.mdx index 6b7f85d8cb5..caca4f8a191 100644 --- a/apps/docs/content/guides/queues/pgmq.mdx +++ b/apps/docs/content/guides/queues/pgmq.mdx @@ -6,7 +6,7 @@ pgmq is a lightweight message queue built on Postgres. ## Features -- Lightweight - No background worker or external dependencies, just Postgres functions packaged in an extension +- Lightweight - No background worker or external dependencies, only Postgres functions packaged in an extension - "exactly once" delivery of messages to a consumer within a visibility timeout - API parity with AWS SQS and RSMQ - Messages stay in the queue until explicitly removed 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. -## Examples + -
-
- - - 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 - -Find the source code and documentation in the Supabase GitHub repository. - -
-
- - 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..0d0b0aee9b6 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 - 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`.
@@ -689,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`. @@ -1042,6 +1042,12 @@ You can configure replay with the following options: - **`since`** (Required): The epoch timestamp in milliseconds (for example, `1697472000000`), specifying the earliest point from which messages should be retrieved. - **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25. + + +Messages are stored in daily partitions, and partitions older than 72 hours are dropped. Because whole days are removed at once, a message stays available for at least 72 hours and at most 4 days, depending on the time of day it was sent. Setting `since` further back than the retained window does not recover deleted messages. See [Realtime Limits](/docs/guides/realtime/limits) for details. + + + -Use the `event` parameter to listen only to database `INSERT`s: + + ```js const changes = supabase @@ -339,7 +348,7 @@ const changes = supabase .on( 'postgres_changes', { - event: 'INSERT', // Listen only to INSERTs + event: 'INSERT', schema: 'public', }, (payload) => console.log(payload) @@ -347,12 +356,75 @@ const changes = supabase .subscribe() ``` + + + +```js +const changes = supabase + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const changes = supabase + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: 'DELETE', + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const changes = supabase + .channel('schema-db-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + <$Show if="sdk:dart"> + + + ```dart -final changes = supabase +supabase .channel('schema-db-changes') .onPostgresChanges( event: PostgresChangeEvent.insert, @@ -361,12 +433,63 @@ final changes = supabase .subscribe(); ``` + + + +```dart +supabase + .channel('schema-db-changes') + .onPostgresChanges( + event: PostgresChangeEvent.update, + schema: 'public', + callback: (payload) => print(payload)) + .subscribe(); +``` + + + + +```dart +supabase + .channel('schema-db-changes') + .onPostgresChanges( + event: PostgresChangeEvent.delete, + schema: 'public', + callback: (payload) => print(payload)) + .subscribe(); +``` + + + + +```dart +supabase + .channel('schema-db-changes') + .onPostgresChanges( + event: PostgresChangeEvent.all, + schema: 'public', + callback: (payload) => print(payload)) + .subscribe(); +``` + + + + <$Show if="sdk:swift"> -Use `InsertAction.self` as type to listen only to database `INSERT`s: +Pass the action type to select the event. + + + ```swift let myChannel = await supabase.channel("schema-db-changes") @@ -380,12 +503,74 @@ for await change in changes { } ``` + + + +```swift +let myChannel = await supabase.channel("schema-db-changes") + +let changes = await myChannel.postgresChange(UpdateAction.self, schema: "public") + +await myChannel.subscribe() + +for await change in changes { + print(change.record) +} +``` + + + + +```swift +let myChannel = await supabase.channel("schema-db-changes") + +let changes = await myChannel.postgresChange(DeleteAction.self, schema: "public") + +await myChannel.subscribe() + +for await change in changes { + print(change.oldRecord) +} +``` + + + + +```swift +let myChannel = await supabase.channel("schema-db-changes") + +let changes = await myChannel.postgresChange(AnyAction.self, schema: "public") + +await myChannel.subscribe() + +for await change in changes { + switch change { + case .insert(let action): print(action.record) + case .update(let action): print(action.record) + case .delete(let action): print(action.oldRecord) + case .select(let action): print(action.record) + } +} +``` + + + + <$Show if="sdk:kotlin"> -Use `PostgresAction.Insert` as type to listen only to database `INSERT`s: +Pass the action type to select the event. + + + ```kotlin val myChannel = supabase.channel("db-changes") @@ -402,91 +587,7 @@ myChannel.subscribe() ``` - -<$Show if="sdk:python"> - - -```python -changes = supabase.channel('schema-db-changes').on_postgres_changes( - "INSERT", # Listen only to INSERTs - schema="public", - callback=lambda payload: print(payload) -) -.subscribe() -``` - - - - - -The channel name can be any string except 'realtime'. - -### Listening to `UPDATE` events - - - - -Use the `event` parameter to listen only to database `UPDATE`s: - -```js -const changes = supabase - .channel('schema-db-changes') - .on( - 'postgres_changes', - { - event: 'UPDATE', // Listen only to UPDATEs - schema: 'public', - }, - (payload) => console.log(payload) - ) - .subscribe() -``` - - -<$Show if="sdk:dart"> - - -```dart -supabase - .channel('schema-db-changes') - .onPostgresChanges( - event: PostgresChangeEvent.update, // Listen only to UPDATEs - schema: 'public', - callback: (payload) => print(payload)) - .subscribe(); -``` - - - -<$Show if="sdk:swift"> - - -Use `UpdateAction.self` as type to listen only to database `UPDATE`s: - -```swift -let myChannel = await supabase.channel("schema-db-changes") - -let changes = await myChannel.postgresChange(UpdateAction.self, schema: "public") - -await myChannel.subscribe() - -for await change in changes { - print(change.oldRecord, change.record) -} -``` - - - -<$Show if="sdk:kotlin"> - - -Use `PostgresAction.Update` as type to listen only to database `UPDATE`s: + ```kotlin val myChannel = supabase.channel("db-changes") @@ -503,91 +604,7 @@ myChannel.subscribe() ``` - -<$Show if="sdk:python"> - - -```python -changes = supabase.channel('schema-db-changes').on_postgres_changes( - "UPDATE", # Listen only to UPDATEs - schema="public", - callback=lambda payload: print(payload) -) -.subscribe() -``` - - - - - -The channel name can be any string except 'realtime'. - -### Listening to `DELETE` events - - - - -Use the `event` parameter to listen only to database `DELETE`s: - -```js -const changes = supabase - .channel('schema-db-changes') - .on( - 'postgres_changes', - { - event: 'DELETE', // Listen only to DELETEs - schema: 'public', - }, - (payload) => console.log(payload) - ) - .subscribe() -``` - - -<$Show if="sdk:dart"> - - -```dart -supabase - .channel('schema-db-changes') - .onPostgresChanges( - event: PostgresChangeEvent.delete, // Listen only to DELETEs - schema: 'public', - callback: (payload) => print(payload)) - .subscribe(); -``` - - - -<$Show if="sdk:swift"> - - -Use `DeleteAction.self` as type to listen only to database `DELETE`s: - -```swift -let myChannel = await supabase.channel("schema-db-changes") - -let changes = await myChannel.postgresChange(DeleteAction.self, schema: "public") - -await myChannel.subscribe() - -for await change in changes { - print(change.oldRecord) -} -``` - - - -<$Show if="sdk:kotlin"> - - -Use `PostgresAction.Delete` as type to listen only to database `DELETE`s: + ```kotlin val myChannel = supabase.channel("db-changes") @@ -603,20 +620,92 @@ changes myChannel.subscribe() ``` + + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") + +changes + .onEach { + when (it) { //You can also check for , etc.. manually + is HasRecord -> println(it.record) + is HasOldRecord -> println(it.oldRecord) + else -> println(it) + } + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + + <$Show if="sdk:python"> + + + ```python changes = supabase.channel('schema-db-changes').on_postgres_changes( - "DELETE", # Listen only to DELETEs + "INSERT", schema="public", callback=lambda payload: print(payload) ) .subscribe() ``` + + + +```python +changes = supabase.channel('schema-db-changes').on_postgres_changes( + "UPDATE", + schema="public", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + +```python +changes = supabase.channel('schema-db-changes').on_postgres_changes( + "DELETE", + schema="public", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + +```python +changes = supabase.channel('schema-db-changes').on_postgres_changes( + "*", + schema="public", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + @@ -955,7 +1044,40 @@ changes = supabase.channel('db-changes').on_postgres_changes( ## Available filters -Realtime offers filters so you can specify the data your client receives at a more granular level. +Realtime offers filters so you can specify the data your client receives at a more granular level. A filter is a `column=operator.value` expression (for example `id=eq.1` or `title=like.%foo%`) that Realtime evaluates on the server, so filtered-out events never leave the database. + +The following operators are available: + +| Operator | Matches when the column… | Example | +| ------------------ | ---------------------------------------------------- | ------------------------- | +| `eq` | equals the value | `id=eq.1` | +| `neq` | does not equal the value | `status=neq.done` | +| `lt` / `lte` | is less than / less than or equal to | `age=lt.65` | +| `gt` / `gte` | is greater than / greater than or equal to | `quantity=gte.10` | +| `in` | is one of a list (max 100 values) | `name=in.(red,blue)` | +| `like` / `ilike` | matches a pattern (case-sensitive / insensitive) | `title=like.%foo%` | +| `match` / `imatch` | matches a POSIX regex (case-sensitive / insensitive) | `slug=match.^post-` | +| `is` | `IS null` / `true` / `false` / `unknown` | `deleted_at=is.null` | +| `isdistinct` | is distinct from the value (NULL-safe `!=`) | `state=isdistinct.active` | + +You can also [negate any operator](#negating-a-filter-not) with `not.` and [combine multiple conditions](#combining-filters-with-and) with commas (applied as an `AND`). + +<$Show if="sdk:js"> + + + +In JavaScript you can pass a raw filter string, or build one with the type-safe `postgresChangesFilter()` helper, which handles operator names, negation, `AND` composition, and escaping for you: + +```js +import { postgresChangesFilter } from '@supabase/supabase-js' + +// → 'quantity=gte.10,status=eq.open' +const filter = postgresChangesFilter().gte('quantity', 10).eq('status', 'open') +``` + + + + ### Equal to (`eq`) @@ -970,6 +1092,34 @@ To listen to changes when a column's value in a table equals a client-specified > + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'messages', + filter: postgresChangesFilter().eq('body', 'hey'), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + ```js const channel = supabase .channel('changes') @@ -986,6 +1136,9 @@ const channel = supabase .subscribe() ``` + + + <$Show if="sdk:dart"> @@ -1085,6 +1238,34 @@ To listen to changes when a column's value in a table does not equal a client-sp > + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages', + filter: postgresChangesFilter().neq('body', 'bye'), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + ```js const channel = supabase .channel('changes') @@ -1101,6 +1282,9 @@ const channel = supabase .subscribe() ``` + + + <$Show if="sdk:dart"> @@ -1201,6 +1385,34 @@ To listen to changes when a column's value in a table is less than a client-spec > + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'profiles', + filter: postgresChangesFilter().lt('age', 65), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + ```js const channel = supabase .channel('changes') @@ -1217,6 +1429,9 @@ const channel = supabase .subscribe() ``` + + + <$Show if="sdk:dart"> @@ -1316,6 +1531,34 @@ To listen to changes when a column's value in a table is less than or equal to a > + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'profiles', + filter: postgresChangesFilter().lte('age', 65), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + ```js const channel = supabase .channel('changes') @@ -1332,6 +1575,9 @@ const channel = supabase .subscribe() ``` + + + <$Show if="sdk:dart"> @@ -1431,6 +1677,34 @@ To listen to changes when a column's value in a table is greater than a client-s > + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'products', + filter: postgresChangesFilter().gt('quantity', 10), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + ```js const channel = supabase .channel('changes') @@ -1447,6 +1721,9 @@ const channel = supabase .subscribe() ``` + + + <$Show if="sdk:dart"> @@ -1546,6 +1823,34 @@ To listen to changes when a column's value in a table is greater than or equal t > + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'products', + filter: postgresChangesFilter().gte('quantity', 10), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + ```js const channel = supabase .channel('changes') @@ -1562,6 +1867,9 @@ const channel = supabase .subscribe() ``` + + + <$Show if="sdk:dart"> @@ -1661,6 +1969,15 @@ To listen to changes when a column's value in a table equals any client-specifie > + + + ```js const channel = supabase .channel('changes') @@ -1670,13 +1987,35 @@ const channel = supabase event: 'INSERT', schema: 'public', table: 'colors', - filter: 'name=in.(red, blue, yellow)', + filter: postgresChangesFilter().in('name', ['red', 'blue', 'yellow']), }, (payload) => console.log(payload) ) .subscribe() ``` + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'colors', + filter: 'name=in.(red,blue,yellow)', + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + <$Show if="sdk:dart"> @@ -1729,7 +2068,7 @@ val myChannel = supabase.channel("db-changes") val changes = myChannel.postgresChangeFlow(schema = "public") { table = "products" - filter = "name=in.(red, blue, yellow)" + filter = "name=in.(red,blue,yellow)" } changes @@ -1751,7 +2090,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( "UPDATE", schema="public", table="products", - filter="name=in.(red, blue, yellow)", + filter="name=in.(red,blue,yellow)", callback=lambda payload: print(payload) ) .subscribe() @@ -1763,6 +2102,970 @@ changes = supabase.channel('db-changes').on_postgres_changes( This filter uses Postgres's `= ANY`. Realtime allows a maximum of 100 values for this filter. +### Pattern matching (`like`, `ilike`) + +To listen to changes when a text column matches a pattern, use `like` (case-sensitive) or `ilike` (case-insensitive). Use `%` to match any sequence of characters and `_` to match a single character. + + + + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'articles', + // matches "Breaking News", "BREAKING", ... + filter: postgresChangesFilter().ilike('title', '%breaking%'), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'articles', + filter: 'title=ilike.%breaking%', // matches "Breaking News", "BREAKING", ... + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + + +<$Show if="sdk:dart"> + + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.insert, + schema: 'public', + table: 'articles', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.ilike, + column: 'title', + value: '%breaking%', + ), + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + InsertAction.self, + schema: "public", + table: "articles", + filter: .ilike("title", value: "%breaking%") +) + +await myChannel.subscribe() + +for await change in changes { + print(change.record) +} +``` + + + +<$Show if="sdk:kotlin"> + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") { + table = "articles" + filter = "title=ilike.%breaking%" +} + +changes + .onEach { + println(it.record) + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + +<$Show if="sdk:python"> + + +```python +changes = supabase.channel('db-changes').on_postgres_changes( + "INSERT", + schema="public", + table="articles", + filter="title=ilike.%breaking%", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + + +`like` uses Postgres's `LIKE` and `ilike` uses `ILIKE`. Both require a text-compatible column. The examples above use `ilike`; swap in `like` for case-sensitive matching—usage is otherwise identical. + +### Regular expression matching (`match`, `imatch`) + +To listen to changes when a text column matches a POSIX regular expression, use `match` (case-sensitive) or `imatch` (case-insensitive). + + + + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'posts', + // matches "post-1", "post-42", ... + filter: postgresChangesFilter().match('slug', '^post-\\d+$'), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'posts', + filter: 'slug=match.^post-\\d+$', // matches "post-1", "post-42", ... + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + + +<$Show if="sdk:dart"> + + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.insert, + schema: 'public', + table: 'posts', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.match, + column: 'slug', + value: r'^post-\d+$', + ), + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + InsertAction.self, + schema: "public", + table: "posts", + filter: .match("slug", value: "^post-\\d+$") +) + +await myChannel.subscribe() + +for await change in changes { + print(change.record) +} +``` + + + +<$Show if="sdk:kotlin"> + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") { + table = "posts" + filter = "slug=match.^post-\\d+$" +} + +changes + .onEach { + println(it.record) + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + +<$Show if="sdk:python"> + + +```python +changes = supabase.channel('db-changes').on_postgres_changes( + "INSERT", + schema="public", + table="posts", + filter="slug=match.^post-\\d+$", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + + +`match` uses Postgres's `~` operator and `imatch` uses `~*`. Both require a text-compatible column, and the pattern is validated when you subscribe. The examples above use `match`; swap in `imatch` for case-insensitive matching—usage is otherwise identical. + +### Null and boolean checks (`is`) + +To listen to changes when a column `IS` `null`, `true`, `false`, or `unknown`, use `is`. `is.null` works on any column type; `is.true`, `is.false`, and `is.unknown` require a boolean column. + + + + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'todos', + // only rows that are not yet completed + filter: postgresChangesFilter().is('completed_at', null), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'todos', + filter: 'completed_at=is.null', // only rows that are not yet completed + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + + +<$Show if="sdk:dart"> + + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.update, + schema: 'public', + table: 'todos', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.isFilter, + column: 'completed_at', + value: null, + ), + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + UpdateAction.self, + schema: "public", + table: "todos", + filter: .is("completed_at", value: .null) +) + +await myChannel.subscribe() + +for await change in changes { + print(change.record) +} +``` + + + +<$Show if="sdk:kotlin"> + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") { + table = "todos" + filter = "completed_at=is.null" +} + +changes + .onEach { + println(it.record) + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + +<$Show if="sdk:python"> + + +```python +changes = supabase.channel('db-changes').on_postgres_changes( + "UPDATE", + schema="public", + table="todos", + filter="completed_at=is.null", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + + +This filter uses Postgres's `IS` operator. + +### Distinct from (`isdistinct`) + +`isdistinct` is a NULL-safe inequality (`IS DISTINCT FROM`). Unlike `neq`, it treats `null` as a comparable value, so a `null` column is considered distinct from a non-null value. + + + + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'orders', + // includes rows where status is null + filter: postgresChangesFilter().isDistinct('status', 'shipped'), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'orders', + filter: 'status=isdistinct.shipped', // includes rows where status is null + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + + +<$Show if="sdk:dart"> + + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.update, + schema: 'public', + table: 'orders', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.isDistinct, + column: 'status', + value: 'shipped', + ), + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + UpdateAction.self, + schema: "public", + table: "orders", + filter: .isDistinct("status", value: "shipped") +) + +await myChannel.subscribe() + +for await change in changes { + print(change.record) +} +``` + + + +<$Show if="sdk:kotlin"> + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") { + table = "orders" + filter = "status=isdistinct.shipped" +} + +changes + .onEach { + println(it.record) + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + +<$Show if="sdk:python"> + + +```python +changes = supabase.channel('db-changes').on_postgres_changes( + "UPDATE", + schema="public", + table="orders", + filter="status=isdistinct.shipped", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + + +### Negating a filter (`not`) + +Prefix any operator with `not.` to invert it — for example `not.in`, `not.is`, or `not.like`. + + + + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'posts', + // anything except drafts and archived + filter: postgresChangesFilter().not('status', 'in', ['draft', 'archived']), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'posts', + filter: 'status=not.in.(draft,archived)', // anything except drafts and archived + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + + +<$Show if="sdk:dart"> + + +Set `negate: true` on a `PostgresChangeFilter` to apply the `not.` prefix. + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.all, + schema: 'public', + table: 'posts', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.inFilter, + column: 'status', + value: ['draft', 'archived'], + negate: true, + ), + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +Wrap any single-condition filter in `.not(...)`. + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + AnyAction.self, + schema: "public", + table: "posts", + filter: .not(.in("status", values: ["draft", "archived"])) +) + +await myChannel.subscribe() +``` + + + +<$Show if="sdk:kotlin"> + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") { + table = "posts" + filter = "status=not.in.(draft,archived)" +} + +changes + .onEach { + println(it.record) + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + +<$Show if="sdk:python"> + + +```python +changes = supabase.channel('db-changes').on_postgres_changes( + "*", + schema="public", + table="posts", + filter="status=not.in.(draft,archived)", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + + +### Combining filters with `AND` + +Combine multiple conditions by separating them with commas. All conditions must match (logical `AND`). You can only combine conditions with `AND` — `OR` is not supported. + + + + + + + +The builder composes conditions and escapes reserved characters for you. + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'orders', + // amount > 100 AND status = "open" + filter: postgresChangesFilter().gt('amount', 100).eq('status', 'open'), + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'orders', + filter: 'amount=gt.100,status=eq.open', // amount > 100 AND status = "open" + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + + + + +<$Show if="sdk:dart"> + + +Pass a list of filters to `filters` to combine them with `AND`. + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.insert, + schema: 'public', + table: 'orders', + filters: [ + PostgresChangeFilter( + type: PostgresChangeFilterType.gt, + column: 'amount', + value: 100, + ), + PostgresChangeFilter( + type: PostgresChangeFilterType.eq, + column: 'status', + value: 'open', + ), + ], + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +Use `.and([...])` to combine multiple conditions. + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + InsertAction.self, + schema: "public", + table: "orders", + filter: .and([ + .gt("amount", value: 100), + .eq("status", value: "open"), + ]) +) + +await myChannel.subscribe() +``` + + + +<$Show if="sdk:kotlin"> + + +```kotlin +val myChannel = supabase.channel("db-changes") + +val changes = myChannel.postgresChangeFlow(schema = "public") { + table = "orders" + filter = "amount=gt.100,status=eq.open" +} + +changes + .onEach { + println(it.record) + } + .launchIn(yourCoroutineScope) + +myChannel.subscribe() +``` + + + +<$Show if="sdk:python"> + + +```python +changes = supabase.channel('db-changes').on_postgres_changes( + "INSERT", + schema="public", + table="orders", + filter="amount=gt.100,status=eq.open", + callback=lambda payload: print(payload) +) +.subscribe() +``` + + + + + + + +Values that contain reserved characters (`,`, `(`, `)`, `"`, or `\`) must be double-quoted PostgREST-style so the server doesn't read them as condition or list boundaries — for example `name=eq."Doe, Jane"`. The `postgresChangesFilter()` builder (JavaScript), `PostgresChangeFilter` (Dart), and `RealtimePostgresFilter` (Swift) apply this quoting for you. + + + +## Selecting specific columns + +By default each change event contains the full row. Use `select` to receive only a subset of columns instead. This reduces payload size and the data transferred per event, which is especially useful for tables with large `bytea`, `jsonb`, or `text` columns. + +The listed columns must be selectable by the subscribing role, and the table's primary key is always included so you can identify the row. `select` requires an explicit `schema` and `table` — it's not supported on wildcard subscriptions. + + + + +```js +const channel = supabase + .channel('changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'profiles', + select: ['id', 'username'], // payload.new only contains { id, username } + }, + (payload) => console.log(payload) + ) + .subscribe() +``` + + +<$Show if="sdk:dart"> + + +```dart +supabase + .channel('changes') + .onPostgresChanges( + event: PostgresChangeEvent.all, + schema: 'public', + table: 'profiles', + select: ['id', 'username'], + callback: (payload) => print(payload)) + .subscribe(); +``` + + + +<$Show if="sdk:swift"> + + +```swift +let myChannel = await supabase.channel("db-changes") + +let changes = await myChannel.postgresChange( + AnyAction.self, + schema: "public", + table: "profiles", + select: ["id", "username"] +) + +await myChannel.subscribe() +``` + + + + + ## Receiving `old` records By default, only `new` record changes are sent but if you want to receive the `old` record (previous values) whenever you `UPDATE` or `DELETE` a record, you can set the `replica identity` of your table to `full`: @@ -1875,7 +3178,7 @@ let changes = await myChannel.postgresChange( UpdateAction.self, schema: "public", table: "products", - filter: "name=in.(red, blue, yellow)" + filter: "name=in.(red,blue,yellow)" ) await myChannel.subscribe() @@ -1900,7 +3203,7 @@ val myChannel = supabase.channel("db-changes") val changes = myChannel.postgresChangeFlow(schema = "public") { table = "products" - filter = "name=in.(red, blue, yellow)" + filter = "name=in.(red,blue,yellow)" } changes @@ -1924,7 +3227,7 @@ changes = supabase.channel('db-changes').on_postgres_changes( "UPDATE", schema="public", table="products", - filter="name=in.(red, blue, yellow)", + filter="name=in.(red,blue,yellow)", callback=lambda payload: print(payload) ) .subscribe() @@ -1934,92 +3237,29 @@ changes = supabase.channel('db-changes').on_postgres_changes( -### Refreshed tokens - -You will need to refresh tokens on your own, but once generated, you can pass them to Realtime. - - - - -For example, if you're using the `supabase-js` `v2` client then you can pass your token like this: - -```js -// Client setup - -supabase.realtime.setAuth('fresh-token') -``` - - -<$Show if="sdk:dart"> - - -```dart -supabase.realtime.setAuth('fresh-token'); -``` - - - -<$Show if="sdk:swift"> - - -```swift -await supabase.realtime.setAuth("fresh-token") -``` - - - -<$Show if="sdk:kotlin"> - - -In Kotlin, you have to update the token manually per channel: - -```kotlin -myChannel.updateAuth("fresh-token") -``` - - - -<$Show if="sdk:python"> - - -```python -supabase.realtime.set_auth('fresh-token') -``` - - - - - ## Limitations ### Delete events are not filterable You can't filter Delete events when tracking Postgres Changes. This limitation is due to the way changes are pulled from Postgres. -### Spaces in table names +## Scaling Postgres Changes -Realtime currently does not work when table names contain spaces. +Postgres Changes authorizes every event against each subscriber. When you make a single change to a table with 100 subscribed users, Realtime performs 100 authorization checks — one per user — so throughput scales with the number of subscribers, not the write rate. Changes are also processed on a single thread to preserve their order, which means larger compute add-ons don't meaningfully increase Postgres Changes throughput. -### Database instance and realtime performance +For most applications this is plenty. To get the best performance: -Realtime systems usually require forethought because of their scaling dynamics. For the `Postgres Changes` feature, every change event must be checked to see if the subscribed user has access. For instance, if you have 100 users subscribed to a table where you make a single insert, it will then trigger 100 "reads": one for each user. +- Use [filters](#available-filters) and [column selection](#selecting-specific-columns) to send each client only the events and columns it needs. +- Keep authorization cheap by writing , indexed [RLS policies](/docs/guides/database/postgres/row-level-security). -There can be a database bottleneck which limits message throughput. If your database cannot authorize the changes rapidly enough, the changes will be delayed until you receive a timeout. - -Database changes are processed on a single thread to maintain the change order. That means compute upgrades don't have a large effect on the performance of Postgres change subscriptions. You can estimate the expected maximum throughput for your database below. - -If you are using Postgres Changes at scale, you should consider using separate "public" table without RLS and filters. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast. - -Enter your database settings to estimate the maximum throughput for your instance: +Use the estimator below to gauge the maximum throughput for your instance, and run your own benchmarks to confirm it fits your use case: -Don't forget to run your own benchmarks to make sure that the performance is acceptable for your use case. + -We are making many improvements to Realtime's Postgres Changes. If you are uncertain about the performance of your use case, reach out using [Support Form](/dashboard/support/new) and we will be happy to help you. We have a team of engineers that can advise you on the best solution for your use-case. +If you expect more than ~3,000 concurrent subscribers on the same changes, use [Broadcast to stream database changes](/docs/guides/realtime/subscribing-to-database-changes#using-broadcast) instead. Broadcast sends each change once and fans it out to all subscribers, so it scales to far higher connection counts than per-subscriber authorization allows. + + + +If you're unsure which approach fits your use case, reach out through the [Support Form](/dashboard/support/new) — our engineers are happy to help you find the best solution. diff --git a/apps/docs/content/guides/realtime/presence.mdx b/apps/docs/content/guides/realtime/presence.mdx index 52c0a9f29e2..2cecc485a81 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 @@ -30,7 +30,7 @@ For high-frequency or fire-and-forget updates, use [Broadcast](/docs/guides/real -During a `sync` event, you may receive `join` and `leave` events simultaneously, even though no users are actually joining or leaving. This is expected behavior—Presence reconciles its local state with the server state, which can trigger these events as part of the synchronization process. This reflects state reconciliation, not real user movement. +During a `sync` event, you may receive `join` and `leave` events simultaneously, even though no users are joining or leaving. This is expected behavior—Presence reconciles its local state with the server state, which can trigger these events as part of the synchronization process. This reflects state reconciliation, not real user movement. diff --git a/apps/docs/content/guides/realtime/protocol.mdx b/apps/docs/content/guides/realtime/protocol.mdx index 5170749dfc2..30e6c5c99f2 100644 --- a/apps/docs/content/guides/realtime/protocol.mdx +++ b/apps/docs/content/guides/realtime/protocol.mdx @@ -35,7 +35,7 @@ Messages can be serialized in different formats. The Realtime protocol supports ## 1.0.0 -Version 1.0.0 is extremely simple. It uses JSON as the serialization format for messages. The underlying WebSocket messages are all text frames. +Version 1.0.0 is minimal. It uses JSON as the serialization format for messages. The underlying WebSocket messages are all text frames. Messages contain the following fields: @@ -214,7 +214,8 @@ This is the initial message required to join a channel. The client sends this me "replay" : { "since": integer, "limit": integer - } + }, + "replication_ready": boolean }, "presence": { "enabled": boolean, @@ -225,7 +226,8 @@ This is the initial message required to join a channel. The client sends this me "event": string, "schema": string, "table": string, - "filter": string + "filter": string, + "select": string[] } ] "private": boolean @@ -242,6 +244,7 @@ This is the initial message required to join a channel. The client sends this me - `replay`: Configuration options for broadcast replay (Optional) - `since`: Replay messages since a specific timestamp in milliseconds - `limit`: Limit the number of replayed messages (Optional) + - `replication_ready`: When `true`, the server emits a `system` event once the Postgres replication connection backing this channel is established and ready to stream changes (Optional). See the [system](#system) event for the payload shape. - `presence`: Configuration options for presence tracking - `enabled`: Whether presence tracking is enabled for this channel - `key`: Key to be used for presence tracking, if not specified or empty, a UUID will be generated and used @@ -249,7 +252,8 @@ This is the initial message required to join a channel. The client sends this me - `event`: Database change event to listen to, accepts `INSERT`, `UPDATE`, `DELETE`, or `*` to listen to all events. - `schema`: Schema of the table to listen to, accepts `*` wildcard to listen to all schemas - `table`: Table of the database to listen to, accepts `*` wildcard to listen to all tables - - `filter`: Filter to be used when pulling changes from database. Read more about filters in the usage docs for [Postgres Changes](/docs/guides/realtime/postgres-changes?queryGroups=language&language=js#filtering-for-specific-changes) + - `filter`: Filter to be used when pulling changes from the database. A filter is a `column=operator.value` expression (for example `id=eq.1` or `title=like.%foo%`). Multiple conditions can be combined with commas and are applied as an `AND` (for example `id=gt.0,id=lt.100`). Any operator can be negated with the `not.` prefix (for example `status=not.in.(draft,archived)`). Reserved characters (`,`, `(`, `)`) inside a value must be double-quoted PostgREST-style (for example `name=eq."a,b"`). See the [Postgres Changes subscription errors](#postgres-changes-subscription-errors) for the full list of supported operators, and the usage docs for [Postgres Changes](/docs/guides/realtime/postgres-changes?queryGroups=language&language=js#filtering-for-specific-changes). + - `select`: Optional array of column names to restrict the change payload to a subset of columns instead of receiving the full row. Reduces payload size and the data transferred per event. The listed columns must be selectable by the subscribing role. Not supported for wildcard (`*`) schema or table subscriptions — an explicit `schema` and `table` are required. - `access_token`: Optional access token for authentication, if not provided, the server will use the API key. Example on protocol version `2.0.0`: @@ -413,7 +417,7 @@ user-event // User Event } ``` -The payload encoding is just a hint for the client to know if the payload should be treated as JSON or not. +The payload encoding is a hint for the client to know if the payload should be treated as JSON or not. {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} @@ -564,7 +568,7 @@ The server sends system messages to inform clients about the status of their Rea - `message`: A human-readable message describing the status of the subscription. - `status`: The status of the subscription, can be `ok`, `error`, or `timeout`. -- `extension`: The extension that sent the message. +- `extension`: The extension that sent the message. `postgres_changes` for Postgres Changes subscription status, or `system` for connection-level messages such as the replication-ready notification. - `channel`: The channel to which the message belongs, such as `realtime:room1`. Example on protocol version `2.0.0`: @@ -584,6 +588,23 @@ Example on protocol version `2.0.0`: ] ``` +When a channel is joined with `config.broadcast.replication_ready` set to `true`, the server sends a `system` message with `extension: "system"` once the Postgres replication connection backing the channel is ready to stream changes. `status` is `"ok"` with `message: "Replication connection established"` on success, or `"error"` if the connection is not established in time (which also closes the channel — see [Channel-level system errors](#channel-level-system-errors)). + +```json +[ + "14", + null, + "realtime:chat-room", + "system", + { + "message": "Replication connection established", + "status": "ok", + "extension": "system", + "channel": "main" + } +] +``` + {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} #### broadcast (text frame) @@ -667,7 +688,7 @@ message // User Event } ``` -The metadata field is JSON encoded. The payload encoding is just a hint for the client to know if the payload should be treated as JSON or not. +The metadata field is JSON encoded. The payload encoding is a hint for the client to know if the payload should be treated as JSON or not. {/* supa-mdx-lint-disable-next-line Rule001HeadingCase */} @@ -715,6 +736,8 @@ The server sends this message when a database change occurs in a subscribed sche - `old_record`: An object representing the old values before the change, with keys as column names and values as their corresponding values. - `errors`: Any errors that occurred during the change, if applicable. +When the subscription was joined with a `select` array (see [phx_join](#phx_join)), `columns`, `record`, and `old_record` are restricted to the selected columns instead of the full row. + ```json [ null, @@ -926,15 +949,16 @@ One exception: the `UnknownErrorOnChannel` code arrives as the bare human-readab `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 | +| 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 | +| `Replication connection was not established in time` | Replication connection not ready before the deadline (only when `replication_ready` was requested) | Retry with backoff | ### Postgres Changes subscription errors @@ -948,7 +972,7 @@ One exception: the `UnknownErrorOnChannel` code arrives as the bare human-readab | 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`. +Supported filter operators: `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `like`, `ilike`, `is`, `match`, `imatch`, `isdistinct`. Any operator can be negated with the `not.` prefix (for example `id=not.eq.5`). Multiple conditions are combined with commas and applied as an `AND` (for example `col1=eq.val,col2=gt.5`). 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. @@ -962,7 +986,7 @@ When `ack` is `true`, the server replies on error with `response.error` (an atom { "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. +Note that the JS client (`send()`) resolves to the string `'error'` and does not expose the specific `error` atom to callers. ### Presence errors diff --git a/apps/docs/content/guides/realtime/reports.mdx b/apps/docs/content/guides/realtime/reports.mdx index 0cf8e04208f..23b10880524 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 @@ -157,7 +157,7 @@ height={625} ## Rate of Channel Joins -The Rate of Channel Joins report helps you monitor how quickly clients are joining Realtime channels over time. This metric is essential for understanding your application's channel subscription patterns and identifying when you're approaching your plan's channel join rate limits. +The Rate of Channel Joins report helps you monitor how fast clients are joining Realtime channels over time. This metric is essential for understanding your application's channel subscription patterns and identifying when you're approaching your plan's channel join rate limits. The report displays the rate of channel joins per second, showing how frequently clients subscribe to channels throughout the selected time period. A channel join occurs whenever a client subscribes to a channel topic to receive real-time updates. Each client connection can join multiple channels (up to 100 per connection for most plans), and the join rate measures how many of these subscriptions happen per second across your entire project. @@ -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,24 +360,24 @@ 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 The Response Speed report helps you monitor the average response time for HTTP requests to the Realtime service over time. This metric is essential for understanding API performance, identifying latency issues, and ensuring your real-time features meet performance expectations. -The report displays the average response time in milliseconds, showing how quickly the Realtime service responds to HTTP requests throughout the selected time period. This includes response times for REST API requests such as broadcast messages, WebSocket upgrade requests, and other HTTP-based interactions. Higher response times can indicate performance bottlenecks, database load issues, or network problems that may impact the real-time responsiveness of your application. +The report displays the average response time in milliseconds, showing how fast the Realtime service responds to HTTP requests throughout the selected time period. This includes response times for REST API requests such as broadcast messages, WebSocket upgrade requests, and other HTTP-based interactions. Higher response times can indicate performance bottlenecks, database load issues, or network problems that may impact the real-time responsiveness of your application. Response Speed chart` 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.