mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
Merge branch 'master' into reviewing_503_guide
This commit is contained in:
1228 files changed
+121391
-71734
No files matched your search
@@ -14,9 +14,9 @@ reviewing PRs that touch tracking or when implementing new tracking.
|
||||
|
||||
**Format:** `[object]_[verb]` in snake_case
|
||||
|
||||
**Approved verbs only:**
|
||||
opened, clicked, submitted, created, removed, updated, retrieved, intended, evaluated, added,
|
||||
enabled, disabled, copied, exposed, failed, converted
|
||||
**Approved verbs only** (canonical list — derived from `packages/common/telemetry-constants.ts`):
|
||||
opened, clicked, submitted, created, removed, updated, intended, evaluated, added,
|
||||
enabled, disabled, copied, exposed, failed, converted, closed, completed, applied, sent, moved
|
||||
|
||||
**Flag these:**
|
||||
- Unapproved verbs (saved, viewed, seen, pressed, etc.)
|
||||
|
||||
@@ -117,10 +117,6 @@ await waitForApiResponse(page, 'pg-meta', ref, 'tables')
|
||||
// ✅ Acceptable - waiting for client-side debounce
|
||||
await page.getByRole('textbox').fill('search term')
|
||||
await page.waitForTimeout(300) // Allow debounce to complete
|
||||
|
||||
// ✅ Acceptable - waiting for clipboard API
|
||||
await page.evaluate(() => navigator.clipboard.readText())
|
||||
await page.waitForTimeout(500)
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
@@ -274,6 +270,22 @@ import {
|
||||
} from '../utils/wait-for-response.js'
|
||||
```
|
||||
|
||||
### Use the existing assertions utilities
|
||||
|
||||
#### Clipboard assertions
|
||||
|
||||
```ts
|
||||
// ❌ Avoid - brittle hard coded timeout
|
||||
await page.evaluate(() => navigator.clipboard.readText())
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
// ✅ Good - this utility function uses Playwright auto-retries mechanisms
|
||||
await expectClipboardValue({
|
||||
page,
|
||||
value: 'expectedValue'
|
||||
})
|
||||
```
|
||||
|
||||
## API Mocking
|
||||
|
||||
### Mock APIs for isolated testing
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copilot Code Review Instructions
|
||||
|
||||
## Repo Context
|
||||
|
||||
This is a TypeScript/Next.js/React monorepo:
|
||||
|
||||
- `apps/studio/` — Supabase Dashboard (primary review target)
|
||||
- `apps/www/` — Marketing site
|
||||
- `apps/docs/` — Documentation
|
||||
- `packages/common/` — Shared code including telemetry definitions
|
||||
|
||||
## Topic-Specific Guidelines
|
||||
|
||||
Detailed review rules are in path-specific instruction files under `.github/instructions/`:
|
||||
|
||||
- **Telemetry**: `studio-telemetry.instructions.md` — event naming, property conventions, feature flag measurement
|
||||
- **Testing**: `studio-testing.instructions.md` — test strategy, extraction patterns, coverage expectations
|
||||
|
||||
These files are scoped to `apps/studio/` and applied automatically by Copilot during reviews.
|
||||
|
||||
## References
|
||||
|
||||
For the full, authoritative versions of these standards:
|
||||
|
||||
- Telemetry: `.claude/skills/telemetry-standards/SKILL.md`
|
||||
- Testing: `.claude/skills/studio-testing/SKILL.md`
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
applyTo: "apps/studio/**,packages/common/telemetry*"
|
||||
---
|
||||
|
||||
# Studio Telemetry Review Rules
|
||||
|
||||
All comments are **advisory** — suggest, do not request changes.
|
||||
|
||||
## When to Flag Missing Telemetry
|
||||
|
||||
Use judgment — not every PR needs telemetry. But **always flag** when:
|
||||
|
||||
1. **Changes to `packages/common/telemetry-constants.ts`** — validate event naming, property conventions, and JSDoc accuracy.
|
||||
2. **PostHog feature flags without measurement.** If a PR uses `usePHFlag` or PostHog-backed hooks like `useDataApiGrantTogglesEnabled` to gate behavior, the flag state should be captured in a telemetry event so the rollout can be measured. Flag if the flag value isn't included in a relevant `track()` call. (Note: `useFlag` from `common` reads ConfigCat flags, not PostHog — different system, different guidance.)
|
||||
3. **Feature-flagged rollouts without outcome tracking.** If a flag gates new behavior, there should be telemetry on both the flag state *and* how users respond to the new behavior (e.g., toggle clicks, opt-in actions).
|
||||
4. **Growth-oriented components adding user interactions without tracking** — onboarding flows, setup wizards, upgrade CTAs, A/B experiment variants.
|
||||
|
||||
When tracking is missing, comment: _"This adds a user interaction (or feature flag) that may benefit from tracking."_ Then propose an event name and `useTrack()` call.
|
||||
|
||||
## Feature Flag Telemetry Pattern
|
||||
|
||||
When capturing a PostHog flag value for telemetry, read the raw flag via `usePHFlag('flagName')` — **not** through wrapper hooks that coerce `undefined` to `false`. Use conditional spread so the property is omitted (not false) when the flag store hasn't loaded:
|
||||
|
||||
```typescript
|
||||
const flagValue = usePHFlag<boolean>('myBooleanFlag') // for boolean flags
|
||||
track('event_name', {
|
||||
...(flagValue !== undefined && { myFlagEnabled: flagValue }),
|
||||
})
|
||||
```
|
||||
|
||||
For string-valued flags (e.g., experiment variants), use `usePHFlag<string>('flagName')` instead.
|
||||
|
||||
## Event Naming
|
||||
|
||||
Format: `[object]_[verb]` in snake_case.
|
||||
|
||||
Prefer verbs already in use in `packages/common/telemetry-constants.ts`: `opened`, `clicked`, `submitted`, `created`, `removed`, `updated`, `intended`, `evaluated`, `added`, `enabled`, `disabled`, `copied`, `exposed`, `failed`, `converted`, `closed`, `completed`, `applied`, `sent`, `moved`.
|
||||
|
||||
Flag: unapproved verbs (`saved`, `viewed`, `pressed`), wrong order (`click_product_card`), wrong casing (`productCardClicked`), passive view tracking on page load (exception: `_exposed` events for A/B experiments).
|
||||
|
||||
## Event Properties
|
||||
|
||||
- **camelCase** for new events; match existing convention when extending
|
||||
- Self-explanatory names — flag generic (`label`, `value`, `name`, `data`)
|
||||
- Check `telemetry-constants.ts` for consistency with similar events
|
||||
- Never track PII
|
||||
|
||||
## Event Implementation
|
||||
|
||||
- Use `useTrack` from `lib/telemetry/track` — avoid introducing new `useSendEventMutation` usage
|
||||
- New events need a TypeScript interface in `telemetry-constants.ts` with `@group Events` and `@source` JSDoc tags (add `@page` when applicable for page-specific events), added to the `TelemetryEvent` union
|
||||
|
||||
```typescript
|
||||
import { useTrack } from 'lib/telemetry/track'
|
||||
const track = useTrack()
|
||||
track('product_card_clicked', { productType: 'database', planTier: 'pro' })
|
||||
```
|
||||
|
||||
Canonical standards: `.claude/skills/telemetry-standards/SKILL.md`
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
applyTo: "apps/studio/**"
|
||||
---
|
||||
|
||||
# Studio Testing Review Rules
|
||||
|
||||
All comments are **advisory**.
|
||||
|
||||
## Core Principle
|
||||
|
||||
Push logic out of React components into pure `.utils.ts` functions, then test those functions exhaustively. Only use component tests for complex UI interactions.
|
||||
|
||||
## When to Comment
|
||||
|
||||
- PR adds **business logic inline in a component** that could be extracted to a `ComponentName.utils.ts` file next to the component and unit tested at `tests/components/.../ComponentName.utils.test.ts`
|
||||
- PR adds a **utility function without test coverage**
|
||||
- PR uses **component tests for pure logic** that should be a unit test on a pure function
|
||||
- PR adds a **feature used in both self-hosted and platform** without E2E test consideration
|
||||
|
||||
## Which Test Type to Suggest
|
||||
|
||||
- **Pure transformation** (parse, format, validate, compute) → extract to `.utils.ts` + unit test with vitest
|
||||
- **Complex UI interaction** → component test with `customRender` (or E2E if shared with self-hosted)
|
||||
- **E2E tests** should cover both click interactions AND keyboard shortcuts
|
||||
- **No tests at all** for non-trivial changes → nudge to add coverage
|
||||
|
||||
## Reference
|
||||
|
||||
See `.claude/skills/studio-testing/SKILL.md` for the full testing standard.
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
sparse-checkout: |
|
||||
apps/docs
|
||||
patches
|
||||
packages/generator
|
||||
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
|
||||
@@ -22,8 +22,10 @@ jobs:
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps
|
||||
packages
|
||||
blocks
|
||||
examples
|
||||
i18n
|
||||
packages
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
@@ -40,56 +42,3 @@ jobs:
|
||||
- name: Run prettier
|
||||
run: |-
|
||||
pnpm run test:prettier
|
||||
|
||||
# i18n is not a node package, so we handle that one separately
|
||||
format-i18n:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
sparse-checkout: |
|
||||
i18n
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Run prettier
|
||||
run: |-
|
||||
pnpm exec prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
|
||||
|
||||
format-sql:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
sparse-checkout: |
|
||||
apps/docs/pages
|
||||
apps/docs/content
|
||||
patches
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Run prettier
|
||||
run: |-
|
||||
# Check mdx files which contain sql code blocks
|
||||
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs pnpm exec prettier -c
|
||||
@@ -25,4 +25,4 @@ jobs:
|
||||
docker/
|
||||
- name: Run docker-compose up
|
||||
# Ensure all services can be started and healthy with default config
|
||||
run: cd docker && cp .env.example .env && docker compose up --wait
|
||||
run: cd docker && cp .env.example .env && docker compose up --wait --wait-timeout 180
|
||||
@@ -70,6 +70,11 @@ jobs:
|
||||
continue-on-error: true
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
sparse-checkout: |
|
||||
packages/ui
|
||||
patches
|
||||
- name: Download coverage artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
|
||||
@@ -1963,6 +1963,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"key-value-field-array-demo": {
|
||||
name: "key-value-field-array-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["button","form","input"],
|
||||
component: React.lazy(() => import("@/registry/default/example/key-value-field-array-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/key-value-field-array-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"field-choice-card": {
|
||||
name: "field-choice-card",
|
||||
type: "components:example",
|
||||
@@ -2678,6 +2689,28 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"error-display-demo": {
|
||||
name: "error-display-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: ["error-display"],
|
||||
component: React.lazy(() => import("@/registry/default/example/error-display-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/error-display-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"error-display-with-children": {
|
||||
name: "error-display-with-children",
|
||||
type: "components:example",
|
||||
registryDependencies: ["error-display"],
|
||||
component: React.lazy(() => import("@/registry/default/example/error-display-with-children")),
|
||||
source: "",
|
||||
files: ["registry/default/example/error-display-with-children.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"logs-bar-chart": {
|
||||
name: "logs-bar-chart",
|
||||
type: "components:example",
|
||||
|
||||
@@ -212,6 +212,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/fragments/status-codes',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Key/Value Field Array',
|
||||
href: '/docs/fragments/key-value-field-array',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,13 +13,11 @@ source:
|
||||
<ComponentPreview name="sheet-demo" peekCode wide />
|
||||
|
||||
1. **Use for side panels**
|
||||
|
||||
- Forms with multiple fields
|
||||
- Settings panels
|
||||
- Detailed editors
|
||||
|
||||
2. **Consider screen size**
|
||||
|
||||
- Sheets work well on desktop
|
||||
- On mobile, consider full-screen or bottom sheet variants
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Key/Value Field Array
|
||||
description: A shared form fragment for repeated text key/value pairs.
|
||||
component: true
|
||||
fragment: true
|
||||
---
|
||||
|
||||
<ComponentPreview
|
||||
name="key-value-field-array-demo"
|
||||
description="A shared form fragment for repeated text key/value pairs."
|
||||
peekCode
|
||||
showDottedGrid
|
||||
wide
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
Use `KeyValueFieldArray` when each row is two text inputs backed by `react-hook-form`, such as HTTP headers, query parameters, or configuration parameters.
|
||||
|
||||
```tsx
|
||||
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
|
||||
```
|
||||
|
||||
```tsx
|
||||
<KeyValueFieldArray
|
||||
control={form.control}
|
||||
name="headers"
|
||||
keyFieldName="name"
|
||||
valueFieldName="value"
|
||||
createEmptyRow={() => ({ name: '', value: '' })}
|
||||
keyPlaceholder="Header name"
|
||||
valuePlaceholder="Header value"
|
||||
addLabel="Add header"
|
||||
/>
|
||||
```
|
||||
|
||||
`KeyValueFieldArray` owns the row add/remove behavior and renders the per-input form messages for you. Compose it inside `FormItemLayout` when you want the standard label, description, and message treatment around the entire section.
|
||||
|
||||
## When to use it
|
||||
|
||||
- Use a plain field array for repeated single values such as redirect URIs.
|
||||
- Use `KeyValueFieldArray` for repeated text/text pairs such as headers, parameters, and config entries.
|
||||
- Build a custom row UI instead when each row mixes different controls, such as a text input paired with a `Select`.
|
||||
@@ -45,14 +45,13 @@ function app() {
|
||||
}
|
||||
```
|
||||
|
||||
**Default props**: All icons default to `size={24}`. Stroke and fill defaults come from the source SVG's root attributes (e.g. `stroke-width="1"` in the source becomes the component's default `strokeWidth`). Override these props as needed for your use case.
|
||||
**Default props**: All icons default to `size={24}`. Stroke and fill defaults come from the source SVG's root attributes (e.g. `stroke-width="1"` on the root `<svg>` becomes the component's default `strokeWidth`). You can override those defaults at the call site, but any `stroke`, `fill`, or `stroke-width` set on individual child paths will still win.
|
||||
|
||||
### Adding new custom icons
|
||||
|
||||
Follow these steps to add a new custom icon to the Supabase icon library.
|
||||
|
||||
1. **Create SVG file**: Add your SVG file to `packages/icons/src/raw-icons/` with a kebab-case name (e.g., `my-new-icon.svg`). Make sure it follows these requirements:
|
||||
|
||||
- Exported at 24×24px with `viewBox="0 0 24 24"`
|
||||
- Uses `stroke="currentColor"` for strokes (no hardcoded colors)
|
||||
- Uses `stroke-width="1.5"` (deviate based on optical weight if necessary)
|
||||
@@ -63,7 +62,7 @@ Follow these steps to add a new custom icon to the Supabase icon library.
|
||||
|
||||
For **fill-only icons** (e.g. logos that use shapes instead of strokes), add `stroke="none"` to the root `<svg>` element. The build will propagate this so the component never renders an unwanted stroke.
|
||||
|
||||
Leave attributes like `stroke-width` as they are. The root SVG's `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` attributes are automatically propagated as the component's defaults by the build process. The conversion to camel-case for React compatibility (e.g. `strokeWidth`) is also handled automatically.
|
||||
Prefer putting shared styling like `fill`, `stroke`, `stroke-width`, `stroke-linecap`, and `stroke-linejoin` on the root `<svg>`. The build propagates those root attributes as the component's defaults, and also converts them to camel-case for React compatibility (e.g. `strokeWidth`). If you put those attributes on individual child paths instead, they become fixed path-level styling and will override props passed to the component.
|
||||
|
||||
2. **Build the component**: Run `npm run build:icons` from inside the `packages/icons` directory
|
||||
|
||||
@@ -133,4 +132,4 @@ Note `stroke="none"` on the root to prevent unwanted strokes, and `fill="current
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If your SVG specifies `stroke-width` attributes, they will override the component's `strokeWidth` prop. Remove stroke attributes from individual paths to let the component control them.
|
||||
If your SVG specifies `stroke-width` attributes on individual child paths, they will override the component's `strokeWidth` prop. Keep shared stroke attributes on the root `<svg>` and remove them from individual paths if you want consumers to control stroke weight.
|
||||
@@ -27,12 +27,20 @@ Forms in side panels (Sheets) use `FormItemLayout` with `layout="horizontal"` on
|
||||
wide
|
||||
/>
|
||||
|
||||
## Field Arrays
|
||||
|
||||
The form previews above include both repeated-field patterns used across Studio:
|
||||
|
||||
- **Field Array** for repeated single-value rows such as redirect URIs.
|
||||
- **Key/Value Field Array** for repeated text pairs such as headers, parameters, and config entries.
|
||||
|
||||
Use the shared [Key/Value Field Array](../fragments/key-value-field-array) fragment when each row is two text inputs managed by `react-hook-form`. Keep using a plain field array when each row is just one input, and build a custom row when the cells are mixed controls.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`.
|
||||
|
||||
2. **Layout selection**:
|
||||
|
||||
- Use `layout="flex-row-reverse"` for page layouts (horizontal alignment)
|
||||
- Use `layout="horizontal"` for side panels with more width
|
||||
- Use `layout="vertical"` for side panels with limited width
|
||||
|
||||
@@ -96,6 +96,7 @@ Implementation checklist:
|
||||
- Render a separate discard confirmation dialog when dirty.
|
||||
- Keep `Cancel` non-destructive; use `Discard`/`Discard changes` for one-click destructive exits.
|
||||
- Guard controlled close attempts only; do not try to block route changes or arbitrary unmounts.
|
||||
- If dismissal is route-driven or tied to page unload, use a navigation guard that composes the same discard-confirmation UI instead of extending the dialog/sheet close guard.
|
||||
|
||||
Studio implementation (preferred in Studio code):
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { format } from 'date-fns'
|
||||
import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
|
||||
import { CalendarIcon, ExternalLink, Plus, Trash, Upload } from 'lucide-react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useFieldArray, useForm } from 'react-hook-form'
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
@@ -61,6 +62,7 @@ const formSchema = z.object({
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
duration: z.number().min(5).max(30),
|
||||
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
|
||||
httpHeaders: z.array(z.object({ key: z.string(), value: z.string() })),
|
||||
apiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -91,6 +93,7 @@ export default function FormPatternsPageLayout() {
|
||||
password: '',
|
||||
duration: 10,
|
||||
redirectUris: [{ value: '' }],
|
||||
httpHeaders: [{ key: '', value: '' }],
|
||||
apiKey: fakeApiKey,
|
||||
},
|
||||
})
|
||||
@@ -285,7 +288,7 @@ export default function FormPatternsPageLayout() {
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Trash2 size={12} />}
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
setLogoFile(undefined)
|
||||
setLogoUrl(undefined)
|
||||
@@ -389,7 +392,7 @@ export default function FormPatternsPageLayout() {
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Trash2 size={12} />}
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
setUploadedFiles((prev) =>
|
||||
prev.filter((_, i) => i !== idx)
|
||||
@@ -669,7 +672,8 @@ export default function FormPatternsPageLayout() {
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Trash2 size={12} />}
|
||||
htmlType="button"
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
)}
|
||||
@@ -679,6 +683,7 @@ export default function FormPatternsPageLayout() {
|
||||
))}
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="button"
|
||||
icon={<Plus />}
|
||||
onClick={() => append({ value: '' })}
|
||||
>
|
||||
@@ -690,6 +695,33 @@ export default function FormPatternsPageLayout() {
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
{/* Key/Value Field Array */}
|
||||
<CardContent>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="httpHeaders"
|
||||
render={() => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Key/Value Field Array"
|
||||
description="Repeated text pairs for headers, parameters, and config entries"
|
||||
>
|
||||
<KeyValueFieldArray
|
||||
control={form.control}
|
||||
name="httpHeaders"
|
||||
keyFieldName="key"
|
||||
valueFieldName="value"
|
||||
createEmptyRow={() => ({ key: '', value: '' })}
|
||||
keyPlaceholder="Header name"
|
||||
valuePlaceholder="Header value"
|
||||
addLabel="Add header"
|
||||
removeLabel="Remove header"
|
||||
/>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
{/* Action Field */}
|
||||
<CardContent>
|
||||
<FormItemLayout
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { format } from 'date-fns'
|
||||
import { CalendarIcon, ExternalLink, Plus, Trash2, Upload } from 'lucide-react'
|
||||
import { CalendarIcon, ExternalLink, Plus, Trash, Upload } from 'lucide-react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useFieldArray, useForm } from 'react-hook-form'
|
||||
import {
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
|
||||
import {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
@@ -58,6 +59,7 @@ const formSchema = z.object({
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
duration: z.number().min(5).max(30),
|
||||
redirectUris: z.array(z.object({ value: z.string().url('Must be a valid URL') })),
|
||||
httpHeaders: z.array(z.object({ key: z.string(), value: z.string() })),
|
||||
apiKey: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -89,6 +91,7 @@ export default function FormPatternsSidePanel() {
|
||||
password: '',
|
||||
duration: 10,
|
||||
redirectUris: [{ value: '' }],
|
||||
httpHeaders: [{ key: '', value: '' }],
|
||||
apiKey: fakeApiKey,
|
||||
},
|
||||
})
|
||||
@@ -303,7 +306,7 @@ export default function FormPatternsSidePanel() {
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Trash2 size={12} />}
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
setLogoFile(undefined)
|
||||
setLogoUrl(undefined)
|
||||
@@ -409,7 +412,7 @@ export default function FormPatternsSidePanel() {
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Trash2 size={12} />}
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => {
|
||||
setUploadedFiles((prev) => prev.filter((_, i) => i !== idx))
|
||||
}}
|
||||
@@ -702,7 +705,8 @@ export default function FormPatternsSidePanel() {
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
icon={<Trash2 size={12} />}
|
||||
htmlType="button"
|
||||
icon={<Trash size={12} />}
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
)}
|
||||
@@ -712,6 +716,7 @@ export default function FormPatternsSidePanel() {
|
||||
))}
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="button"
|
||||
icon={<Plus />}
|
||||
onClick={() => append({ value: '' })}
|
||||
>
|
||||
@@ -725,6 +730,37 @@ export default function FormPatternsSidePanel() {
|
||||
|
||||
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
||||
|
||||
{/* Key/Value Field Array */}
|
||||
<SheetSection>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="httpHeaders"
|
||||
render={() => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label="Key/Value Field Array"
|
||||
description="Repeated text pairs for headers, parameters, and config entries"
|
||||
>
|
||||
<div className="col-span-6">
|
||||
<KeyValueFieldArray
|
||||
control={form.control}
|
||||
name="httpHeaders"
|
||||
keyFieldName="key"
|
||||
valueFieldName="value"
|
||||
createEmptyRow={() => ({ key: '', value: '' })}
|
||||
keyPlaceholder="Header name"
|
||||
valuePlaceholder="Header value"
|
||||
addLabel="Add header"
|
||||
removeLabel="Remove header"
|
||||
/>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</SheetSection>
|
||||
|
||||
<Separator className="-mx-5 w-[calc(100%+2.5rem)]" />
|
||||
|
||||
{/* Action Field */}
|
||||
<SheetSection>
|
||||
<FormItemLayout
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Button, Form_Shadcn_ } from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { KeyValueFieldArray } from 'ui-patterns/form/KeyValueFieldArray/KeyValueFieldArray'
|
||||
import { z } from 'zod'
|
||||
|
||||
const formSchema = z.object({
|
||||
headers: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export default function KeyValueFieldArrayDemo() {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
headers: [{ name: 'x-client-info', value: 'studio-docs' }],
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
console.log(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form className="w-full max-w-2xl" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormItemLayout
|
||||
label="HTTP headers"
|
||||
description="Use KeyValueFieldArray for repeated text key/value pairs."
|
||||
>
|
||||
<KeyValueFieldArray
|
||||
control={form.control}
|
||||
name="headers"
|
||||
keyFieldName="name"
|
||||
valueFieldName="value"
|
||||
createEmptyRow={() => ({ name: '', value: '' })}
|
||||
keyPlaceholder="Header name"
|
||||
valuePlaceholder="Header value"
|
||||
addLabel="Add header"
|
||||
removeLabel="Remove header"
|
||||
/>
|
||||
</FormItemLayout>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button size="tiny" type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -1107,6 +1107,12 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/form-item-layout-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'key-value-field-array-demo',
|
||||
type: 'components:example',
|
||||
registryDependencies: ['button', 'form', 'input'],
|
||||
files: ['example/key-value-field-array-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'field-choice-card',
|
||||
type: 'components:example',
|
||||
|
||||
@@ -14,7 +14,10 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
return new NextResponse(content, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=3600',
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
|
||||
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
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'
|
||||
@@ -83,13 +83,10 @@ const getContent = async ({ slug }: Params) => {
|
||||
|
||||
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
|
||||
|
||||
const response = await fetchRevalidatePerDay(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
const content = removeRedundantH1(
|
||||
await getGitHubFileContents({ org, repo, path: `${docsDir}/${remoteFile}`, branch })
|
||||
)
|
||||
|
||||
let content = await response.text()
|
||||
content = removeRedundantH1(content)
|
||||
|
||||
return {
|
||||
pathname: `/guides/ai/python/${slug}` satisfies `/${string}`,
|
||||
meta,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import { Heading } from 'ui'
|
||||
@@ -7,7 +6,7 @@ import { Admonition } from 'ui-patterns'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { OCTOKIT_RETRY_OPTIONS, getGitHubFileContents, octokit } from '~/lib/octokit'
|
||||
import { TabPanel, Tabs } from '~/features/ui/Tabs'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
@@ -144,9 +143,7 @@ const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (
|
||||
* Fetch lint remediation Markdown from external repo
|
||||
*/
|
||||
const getLints = async () => {
|
||||
const octokit = new Octokit({ request: { fetch: fetchRevalidatePerDay } })
|
||||
|
||||
const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
const response = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner: org,
|
||||
repo: repo,
|
||||
path: docsDir,
|
||||
@@ -154,10 +151,13 @@ const getLints = async () => {
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
request: OCTOKIT_RETRY_OPTIONS,
|
||||
})
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw Error(`Could not get contents of repo ${org}/${repo}`)
|
||||
throw new Error(
|
||||
`Failed to fetch ${org}/${repo}/contents/${docsDir} docs from GitHub: ${response.status}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(response.data)) {
|
||||
@@ -170,15 +170,7 @@ const getLints = async () => {
|
||||
|
||||
const lints = await Promise.all(
|
||||
lintsList.map(async ({ path }) => {
|
||||
const fileResponse = await fetchRevalidatePerDay(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}`
|
||||
)
|
||||
|
||||
if (fileResponse.status >= 400) {
|
||||
throw Error(`Could not get contents of file ${org}/${repo}/${path}`)
|
||||
}
|
||||
|
||||
const content = await fileResponse.text()
|
||||
const content = await getGitHubFileContents({ org, repo, path, branch })
|
||||
|
||||
return {
|
||||
path: getBasename(path),
|
||||
|
||||
@@ -360,10 +360,22 @@ const getContent = async (params: Params) => {
|
||||
const repoPath = `${org}/${repo}/${tag}/${docsDir}/${remoteFile}`
|
||||
editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}`
|
||||
|
||||
const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, {
|
||||
cache: 'force-cache',
|
||||
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
|
||||
})
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, {
|
||||
cache: 'force-cache',
|
||||
next: { tags: [REVALIDATION_TAGS.WRAPPERS] },
|
||||
})
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch wrappers docs from GitHub (network error): ${err}`)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch wrappers docs from GitHub: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const rawContent = await response.text()
|
||||
|
||||
assetsBaseUrl = `https://raw.githubusercontent.com/${org}/${repo}/${tag}/docs/assets/`
|
||||
|
||||
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
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'
|
||||
@@ -82,12 +82,10 @@ const getContent = async ({ slug }: Params) => {
|
||||
|
||||
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
|
||||
|
||||
const response = await fetchRevalidatePerDay(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
|
||||
const content = removeRedundantH1(
|
||||
await getGitHubFileContents({ org, repo, path: `${docsDir}/${remoteFile}`, branch })
|
||||
)
|
||||
|
||||
const content = removeRedundantH1(await response.text())
|
||||
|
||||
return {
|
||||
pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`,
|
||||
meta,
|
||||
|
||||
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { isValidGuideFrontmatter } from '~/lib/docs'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
@@ -115,11 +115,12 @@ const getContent = async ({ slug }: Params) => {
|
||||
`${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}`
|
||||
)
|
||||
|
||||
let response = await fetchRevalidatePerDay(
|
||||
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}`
|
||||
)
|
||||
|
||||
let rawContent = await response.text()
|
||||
let rawContent = await getGitHubFileContents({
|
||||
org: terraformDocsOrg,
|
||||
repo: terraformDocsRepo,
|
||||
path: useRoot ? remoteFile : `${terraformDocsDocsDir}/${remoteFile}`,
|
||||
branch: terraformDocsBranch,
|
||||
})
|
||||
// Strip out HTML comments
|
||||
rawContent = rawContent.replace(/<!--.*?-->/, '')
|
||||
let { content, data } = matter(rawContent)
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { getGitHubFileContents } from '~/lib/octokit'
|
||||
import { TabPanel, Tabs } from '~/features/ui/Tabs'
|
||||
import {
|
||||
terraformDocsBranch,
|
||||
@@ -394,12 +394,14 @@ const TerraformReferencePage = async () => {
|
||||
* Fetch JSON schema from external repo
|
||||
*/
|
||||
const getSchema = async () => {
|
||||
let response = await fetchRevalidatePerDay(
|
||||
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${terraformDocsDocsDir}/schema.json`
|
||||
const schema = JSON.parse(
|
||||
await getGitHubFileContents({
|
||||
org: terraformDocsOrg,
|
||||
repo: terraformDocsRepo,
|
||||
path: `${terraformDocsDocsDir}/schema.json`,
|
||||
branch: terraformDocsBranch,
|
||||
})
|
||||
)
|
||||
if (!response.ok) throw Error('Failed to fetch Terraform JSON schema from GitHub')
|
||||
|
||||
const schema = await response.json()
|
||||
|
||||
return {
|
||||
schema,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import matter from 'gray-matter'
|
||||
import { cache } from 'react'
|
||||
|
||||
import { OCTOKIT_RETRY_OPTIONS, getGitHubFileContents, octokit } from '~/lib/octokit'
|
||||
|
||||
const SKILLS_REPO = {
|
||||
org: 'supabase',
|
||||
repo: 'agent-skills',
|
||||
branch: 'main',
|
||||
path: 'skills',
|
||||
}
|
||||
|
||||
interface SkillMetadata {
|
||||
name?: string
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface SkillSummary {
|
||||
name: string
|
||||
description: string
|
||||
installCommand: string
|
||||
}
|
||||
|
||||
async function getAiSkillsImpl(): Promise<SkillSummary[]> {
|
||||
const { data: contents } = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner: SKILLS_REPO.org,
|
||||
repo: SKILLS_REPO.repo,
|
||||
path: SKILLS_REPO.path,
|
||||
ref: SKILLS_REPO.branch,
|
||||
request: OCTOKIT_RETRY_OPTIONS,
|
||||
})
|
||||
|
||||
if (!Array.isArray(contents)) {
|
||||
throw new Error('Expected directory listing from GitHub agent skills repo')
|
||||
}
|
||||
|
||||
const skillDirs = contents.filter((item) => item.type === 'dir')
|
||||
|
||||
const skills = await Promise.all(
|
||||
skillDirs.map(async (item) => {
|
||||
const skillPath = `${SKILLS_REPO.path}/${item.name}/SKILL.md`
|
||||
const rawContent = await getGitHubFileContents({
|
||||
org: SKILLS_REPO.org,
|
||||
repo: SKILLS_REPO.repo,
|
||||
branch: SKILLS_REPO.branch,
|
||||
path: skillPath,
|
||||
})
|
||||
const { data } = matter(rawContent) as { data: SkillMetadata }
|
||||
|
||||
return {
|
||||
name: item.name,
|
||||
description: data.description || '',
|
||||
installCommand: `npx skills add supabase/agent-skills --skill ${item.name}`,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return skills.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export const getAiSkills = cache(getAiSkillsImpl)
|
||||
@@ -0,0 +1,60 @@
|
||||
import { getAiSkills } from './AiSkills.utils'
|
||||
import { CopyButton } from './CopyButton'
|
||||
|
||||
export async function AiSkillsIndex() {
|
||||
let skills: Awaited<ReturnType<typeof getAiSkills>> = []
|
||||
|
||||
try {
|
||||
skills = await getAiSkills()
|
||||
} catch {
|
||||
// Swallow errors from getAiSkills to keep the page usable
|
||||
}
|
||||
|
||||
if (!skills.length) {
|
||||
return (
|
||||
<div className="not-prose text-sm text-foreground-lighter">
|
||||
Unable to load AI skills at the moment.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="not-prose overflow-x-auto">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-default">
|
||||
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Skill</th>
|
||||
<th className="text-left py-2 pr-4 text-foreground-lighter font-medium">Description</th>
|
||||
<th className="text-left py-2 text-foreground-lighter font-medium">Install command</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{skills.map((skill) => (
|
||||
<tr key={skill.name} className="border-b border-default">
|
||||
<td className="py-3 pr-4 font-mono text-xs whitespace-nowrap">
|
||||
<a
|
||||
href={`https://github.com/supabase/agent-skills/tree/main/skills/${skill.name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground hover:text-brand transition-colors"
|
||||
>
|
||||
{skill.name}
|
||||
</a>
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-foreground-lighter">{skill.description}</td>
|
||||
<td className="w-px p-0">
|
||||
<div className="h-full max-w-xs overflow-x-auto flex items-center py-3">
|
||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
<CopyButton text={skill.installCommand} />
|
||||
<code className="font-mono text-xs text-foreground-lighter">
|
||||
{skill.installCommand}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-md border p-1.5 transition-colors',
|
||||
'border-default bg-surface-100 text-foreground-lighter hover:bg-surface-200 hover:text-foreground'
|
||||
)}
|
||||
title={copied ? 'Copied!' : 'Copy to clipboard'}
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import Layout from '~/layouts/guides'
|
||||
import { getAiPrompts } from '../getting-started/ai-prompts/[slug]/AiPrompts.utils'
|
||||
|
||||
export default async function GettingStartedLayout({ children }: { children: React.ReactNode }) {
|
||||
const additionalNavItems = { prompts: await getPrompts() }
|
||||
const additionalNavItems = {
|
||||
prompts: await getPrompts(),
|
||||
}
|
||||
|
||||
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
|
||||
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'
|
||||
@@ -64,7 +64,7 @@ const pageMap = [
|
||||
title: 'Computed Fields',
|
||||
subtitle: 'Using Postgres Computed Fields with GraphQL.',
|
||||
},
|
||||
remoteFile: 'computed-fields.md',
|
||||
remoteFile: 'computed_fields.md',
|
||||
},
|
||||
{
|
||||
slug: 'configuration',
|
||||
@@ -136,12 +136,12 @@ const getContent = async ({ slug }: Params) => {
|
||||
|
||||
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
|
||||
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`,
|
||||
{ cache: 'force-cache', next: { tags: [REVALIDATION_TAGS.GRAPHQL] } }
|
||||
)
|
||||
|
||||
const content = await response.text()
|
||||
const content = await getGitHubFileContents({
|
||||
org,
|
||||
repo,
|
||||
path: `${docsDir}/${remoteFile}`,
|
||||
branch,
|
||||
})
|
||||
|
||||
return {
|
||||
pathname: `/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
|
||||
|
||||
@@ -176,6 +176,18 @@ function useBreadcrumbs() {
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
// TODO: Breadcrumbs currently can't infer the "AI Tools" parent for /guides/getting-started/ai-* routes,
|
||||
// so we special-case these paths here. Remove when Breadcrumbs can derive this hierarchy from NavigationMenu.
|
||||
const isAiSkillsPage = pathname.startsWith('/guides/getting-started/ai-skills')
|
||||
if (isAiSkillsPage) {
|
||||
const breadcrumbs = [
|
||||
{ name: 'Getting started', url: '/guides/getting-started' },
|
||||
{ name: 'AI Tools' },
|
||||
{ name: 'Agent Skills', url: '/guides/getting-started/ai-skills' },
|
||||
]
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const menuId = getMenuId(pathname)
|
||||
const menu = NavItems[menuId]
|
||||
return findMenuItemByUrl(menu, pathname, [])
|
||||
|
||||
@@ -30,7 +30,15 @@ function AiTools({ className }: { className?: string }) {
|
||||
|
||||
try {
|
||||
const res = await fetch(mdUrl)
|
||||
const text = await res.text()
|
||||
let text: string
|
||||
|
||||
if (res.ok) {
|
||||
text = await res.text()
|
||||
} else {
|
||||
// Default to HTML content within the article when no .md file is available.
|
||||
text = document.getElementById('sb-docs-guide-main-article')?.innerHTML ?? ''
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
|
||||
@@ -499,6 +499,10 @@ export const gettingstarted: NavMenuConstant = {
|
||||
name: 'Prompts',
|
||||
url: '/guides/getting-started/ai-prompts' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Agent Skills',
|
||||
url: '/guides/getting-started/ai-skills' as `/${string}`,
|
||||
},
|
||||
{
|
||||
name: 'Supabase MCP server',
|
||||
url: '/guides/getting-started/mcp' as `/${string}`,
|
||||
@@ -773,6 +777,12 @@ export const auth: NavMenuConstant = {
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Custom OAuth/OIDC Providers',
|
||||
url: '/guides/auth/custom-oauth-providers',
|
||||
enabled: allAuthProvidersEnabled,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Anonymous Sign-Ins',
|
||||
url: '/guides/auth/auth-anonymous',
|
||||
@@ -1547,6 +1557,11 @@ export const graphql: NavMenuConstant = {
|
||||
{ name: 'API', url: '/guides/graphql/api', items: [] },
|
||||
{ name: 'Views', url: '/guides/graphql/views', items: [] },
|
||||
{ name: 'Functions', url: '/guides/graphql/functions', items: [] },
|
||||
{
|
||||
name: 'Computed Fields',
|
||||
url: '/guides/graphql/computed-fields',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Configuration & Customization',
|
||||
url: '/guides/graphql/configuration',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<Price price="2" /> per 1 million invocations. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
<Price price="2" /> per 1 million invocations. You are only charged for usage exceeding your
|
||||
subscription plan's quota.
|
||||
|
||||
| Plan | Quota | Over-Usage |
|
||||
| ---------- | --------- | --------------------------------------------- |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Pricing
|
||||
|
||||
<Price price="0.015" /> per SSO MAU. You are only charged for usage exceeding your subscription plan's
|
||||
quota.
|
||||
<Price price="0.015" /> per SSO MAU. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
|
||||
For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active SSO Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-sso).
|
||||
@@ -1,6 +1,6 @@
|
||||
## Pricing
|
||||
|
||||
<Price price="0.00325" /> per Third-Party MAU. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
<Price price="0.00325" /> per Third-Party MAU. You are only charged for usage exceeding your
|
||||
subscription plan's quota.
|
||||
|
||||
For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active Third-Party Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party).
|
||||
@@ -1,7 +1,7 @@
|
||||
## Pricing
|
||||
|
||||
<Price price="0.1027" /> per hour (<Price price="75" /> per month) for the first project. <Price price="0.0137" /> per
|
||||
hour (<Price price="10" /> per month) for every additional project.
|
||||
<Price price="0.1027" /> per hour (<Price price="75" /> per month) for the first project.
|
||||
<Price price="0.0137" /> per hour (<Price price="10" /> per month) for every additional project.
|
||||
|
||||
| Plan | Project 1 per month | Project 2 per month | Project 3 per month |
|
||||
| ---------- | -------------------- | -------------------- | -------------------- |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<Price price="10" /> per 1,000 peak connections. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
<Price price="10" /> per 1,000 peak connections. You are only charged for usage exceeding your
|
||||
subscription plan's quota.
|
||||
|
||||
| Plan | Quota | Over-Usage |
|
||||
| ---------- | ------ | ----------------------------------------------- |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<Price price="2.50" /> per 1 million messages. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
<Price price="2.50" /> per 1 million messages. You are only charged for usage exceeding your
|
||||
subscription plan's quota.
|
||||
|
||||
| Plan | Quota | Over-Usage |
|
||||
| ---------- | --------- | --------------------------------------------- |
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<Price price="5" /> per 1,000 origin images. You are only charged for usage exceeding your subscription
|
||||
plan's quota.
|
||||
<Price price="5" /> per 1,000 origin images. You are only charged for usage exceeding your
|
||||
subscription plan's quota.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ Supabase Auth works with many popular Auth methods, including Social and Phone A
|
||||
|
||||
<AuthProviders type="social" />
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
You can also add any OAuth2 or OIDC-compatible identity provider using [Custom OAuth/OIDC Providers](/docs/guides/auth/custom-oauth-providers).
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Phone Auth
|
||||
|
||||
<AuthProviders type="phone" />
|
||||
@@ -5,7 +5,6 @@ For a better user experience, you can use **Universal Links** instead of custom
|
||||
To enable Universal Links, you need to:
|
||||
|
||||
1. **Configure Associated Domains** in your Xcode project:
|
||||
|
||||
- Add your domain to the Associated Domains capability
|
||||
- Format: `applinks:yourdomain.com`
|
||||
|
||||
|
||||
@@ -134,13 +134,11 @@ $$;
|
||||
Let's break this down:
|
||||
|
||||
- **Parameters:** The function accepts quite a few parameters, but the main (required) ones are `query_text`, `query_embedding`, and `match_count`.
|
||||
|
||||
- `query_text` is the user's query text (more on this shortly)
|
||||
- `query_embedding` is the vector representation of the user's query produced by the embedding model. We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model. This must match the size of the `embedding` vector on the `documents` table (and use the same model).
|
||||
- `match_count` is the number of records returned in the `limit` clause.
|
||||
|
||||
The other parameters are optional, but give more control over the fusion process.
|
||||
|
||||
- `full_text_weight` and `semantic_weight` decide how much weight each search method gets in the final score. These are both 1 by default which means they both equally contribute towards the final rank. A `full_text_weight` of 2 and `semantic_weight` of 1 would give full-text search twice as much weight as semantic search.
|
||||
- `rrf_k` is the `k` [smoothing constant](#smoothing-constant-k) added to the reciprocal rank. The default is 50.
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
|
||||
|
||||
When `email_action_type` is `email_change`, the hook payload can include one or two OTPs and their hashes. This depends on your [Secure Email Change](/dashboard/project/_/auth/providers?provider=Email) setting.
|
||||
|
||||
- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.email_new`). You must send two emails.
|
||||
- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.new_email`). You must send two emails.
|
||||
- Secure Email Change disabled: only one OTP is generated for the new email. You send a single email.
|
||||
|
||||
<Admonition type="caution" title="Counterintuitive field naming">
|
||||
@@ -380,7 +380,7 @@ When `email_action_type` is `email_change`, the hook payload can include one or
|
||||
The token hash field names are reversed due to backward compatibility. Pay careful attention to which token/hash pair goes with which email address:
|
||||
|
||||
- `token_hash_new` → use with the **current** email address (`user.email`) and `token`
|
||||
- `token_hash` → use with the **new** email address (`user.email_new`) and `token_new`
|
||||
- `token_hash` → use with the **new** email address (`user.new_email`) and `token_new`
|
||||
|
||||
Do not assume the `_new` suffix refers to the new email address.
|
||||
|
||||
@@ -391,7 +391,7 @@ Do not assume the `_new` suffix refers to the new email address.
|
||||
When Secure Email Change is enabled (both token/hash pairs present):
|
||||
|
||||
- Send to **current** email address (`user.email`): use `token` with `token_hash_new`
|
||||
- Send to **new** email address (`user.email_new`): use `token_new` with `token_hash`
|
||||
- Send to **new** email address (`user.new_email`): use `token_new` with `token_hash`
|
||||
|
||||
When Secure Email Change is **disabled** (only one token/hash pair present):
|
||||
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
---
|
||||
id: 'custom-oauth-providers'
|
||||
title: 'Custom OAuth/OIDC Providers'
|
||||
description: 'Add any OAuth2 or OIDC-compatible identity provider to your Supabase project'
|
||||
---
|
||||
|
||||
Custom OAuth/OIDC providers let you integrate any standards-compliant identity provider with Supabase Auth, beyond the ones Supabase supports out of the box.
|
||||
|
||||
Each custom provider uses a `custom:` prefix in its identifier (for example, `custom:my-idp` or `custom:github-enterprise`). This prefix distinguishes custom providers from built-in providers.
|
||||
|
||||
There are two provider types:
|
||||
|
||||
- **OAuth2**: for generic OAuth2 providers where you supply the authorization, token, and userinfo endpoints manually.
|
||||
- **OIDC**: for providers that support [OpenID Connect](https://openid.net/connect/) discovery. You supply only the issuer URL and endpoints are resolved automatically.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
You can add up to 3 custom providers per project. If you need more, [contact support](/dashboard/support/new).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Creating a provider
|
||||
|
||||
The create form displays a read-only **Callback URL**. Copy this URL and configure it as the redirect/callback URI in your external identity provider before completing setup.
|
||||
|
||||
### OAuth2 provider
|
||||
|
||||
Use an OAuth2 provider when your identity provider does not support OpenID Connect discovery. You must supply the authorization, token, and userinfo endpoint URLs explicitly.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
queryGroup="setup-method"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
|
||||
2. Click **New Provider**. Select **Manual configuration** as the configuration method.
|
||||
3. Enter a unique identifier (must start with `custom:`, for example `custom:my-oauth-provider`).
|
||||
4. Enter the provider's **Client ID** and **Client Secret**.
|
||||
5. Enter the **Authorization URL**, **Token URL**, and **UserInfo URL**.
|
||||
6. Click **Create and enable provider**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
|
||||
provider_type: 'oauth2',
|
||||
identifier: 'custom:my-oauth-provider',
|
||||
name: 'My OAuth Provider',
|
||||
client_id: 'your-client-id',
|
||||
client_secret: 'your-client-secret',
|
||||
authorization_url: 'https://provider.example.com/oauth/authorize',
|
||||
token_url: 'https://provider.example.com/oauth/token',
|
||||
userinfo_url: 'https://provider.example.com/oauth/userinfo',
|
||||
scopes: ['profile', 'email'],
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### OIDC provider
|
||||
|
||||
Use an OIDC provider when your identity provider supports OpenID Connect. Supply the `issuer` URL and the discovery document, JWKS, and endpoints are resolved automatically.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
queryGroup="setup-method"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
|
||||
2. Click **New Provider**. Select **Auto-discovery (OIDC)** as the configuration method.
|
||||
3. Enter a unique identifier (must start with `custom:`, for example `custom:my-regional-provider`).
|
||||
4. Enter the provider's **Client ID** and **Client Secret**.
|
||||
5. Enter the **Issuer URL**. The discovery document and endpoints are resolved automatically.
|
||||
6. Click **Create and enable provider**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
|
||||
provider_type: 'oidc',
|
||||
identifier: 'custom:my-regional-provider',
|
||||
name: 'Regional Provider',
|
||||
client_id: 'your-client-id',
|
||||
client_secret: 'your-client-secret',
|
||||
issuer: 'https://auth.example.com',
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
OIDC providers have the following automatic behavior:
|
||||
|
||||
- The discovery document is fetched from `{issuer}/.well-known/openid-configuration` (or from `discovery_url` if set).
|
||||
- The `openid` scope is always included. It is automatically added if missing from the `scopes` array.
|
||||
- ID tokens are verified against the provider's JWKS (fetched from the discovery document's `jwks_uri`).
|
||||
|
||||
## Provider identifiers
|
||||
|
||||
Every custom provider identifier must start with the `custom:` prefix. Identifiers are 2–50 characters, lowercase alphanumeric with hyphens and colons allowed. Examples:
|
||||
|
||||
- `custom:my-provider`
|
||||
- `custom:github-enterprise`
|
||||
|
||||
## User sign-in
|
||||
|
||||
Once a custom provider is created and enabled, users sign in via the standard OAuth authorize endpoint:
|
||||
|
||||
```
|
||||
GET https://your-project.supabase.co/auth/v1/authorize?provider=custom:my-provider
|
||||
```
|
||||
|
||||
Or using the Supabase client libraries:
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="js"
|
||||
queryGroup="language"
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'custom:my-provider',
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="flutter" label="Flutter">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithOAuth(
|
||||
OAuthProvider('custom:my-provider'),
|
||||
);
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="swift" label="Swift">
|
||||
|
||||
```swift
|
||||
try await supabase.auth.signInWithOAuth(
|
||||
provider: "custom:my-provider",
|
||||
redirectTo: URL(string: "my-custom-scheme://my-app-host")
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
```kotlin
|
||||
supabase.auth.signInWith(CustomProvider("custom:my-provider"))
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Managing providers
|
||||
|
||||
### List providers
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
queryGroup="setup-method"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard. All custom providers are listed under **Custom OAuth Providers**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
// List all custom providers
|
||||
const { data, error } = await supabase.auth.admin.customProviders.listProviders()
|
||||
|
||||
// Filter by provider type
|
||||
const { data, error } = await supabase.auth.admin.customProviders.listProviders({
|
||||
type: 'oidc',
|
||||
})
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Update a provider
|
||||
|
||||
Update any provider fields except `provider_type` and `identifier`. Only provided fields are changed (partial update). To rotate a client secret, update only the `client_secret` field.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
queryGroup="setup-method"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
|
||||
2. Click the three-dot menu (⋮) next to the provider and select **Update**.
|
||||
3. Modify the fields you want to change.
|
||||
4. Click **Update provider**.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.admin.customProviders.updateProvider(
|
||||
'custom:my-provider',
|
||||
{
|
||||
name: 'Updated Provider Name',
|
||||
scopes: ['profile', 'email', 'groups'],
|
||||
enabled: false,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Delete a provider
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="dashboard"
|
||||
queryGroup="setup-method"
|
||||
>
|
||||
<TabPanel id="dashboard" label="Dashboard">
|
||||
|
||||
1. Go to [Auth Providers](/dashboard/project/_/auth/providers) in the Dashboard.
|
||||
2. Click the three-dot menu (⋮) next to the provider and select **Delete**.
|
||||
3. Confirm the deletion.
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
```js
|
||||
const { data, error } =
|
||||
await supabase.auth.admin.customProviders.deleteProvider('custom:my-provider')
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
### PKCE
|
||||
|
||||
PKCE (Proof Key for Code Exchange) is enabled by default (`pkce_enabled: true`) for all custom providers. The auth server automatically generates a code challenge and verifier during the authorization flow, protecting against authorization code interception attacks. This is handled entirely server-side, no client-side PKCE logic is needed.
|
||||
|
||||
To disable PKCE for a specific provider, set `pkce_enabled: false` when creating or updating it. This is not recommended unless the identity provider does not support PKCE.
|
||||
|
||||
### Authorization params
|
||||
|
||||
Extra query parameters appended to the provider's authorization URL during the OAuth flow. All values must be strings.
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "consent",
|
||||
"access_type": "offline",
|
||||
"login_hint": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
The following reserved parameters are managed by the auth server and cannot be overridden: `client_id`, `client_secret`, `redirect_uri`, `response_type`, `state`, `code_challenge`, `code_challenge_method`, `code_verifier`, `nonce`.
|
||||
|
||||
### Multi-platform apps
|
||||
|
||||
If your app uses different client IDs for different platforms (for example, web vs mobile), use `acceptable_client_ids` to list additional client IDs that should be accepted for audience validation in OIDC ID tokens:
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
|
||||
provider_type: 'oidc',
|
||||
identifier: 'custom:multi-platform-app',
|
||||
name: 'Multi-Platform App',
|
||||
client_id: 'web-client-id',
|
||||
client_secret: 'your-client-secret',
|
||||
issuer: 'https://app.example.com',
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
acceptable_client_ids: ['ios-client-id', 'android-client-id'],
|
||||
})
|
||||
```
|
||||
|
||||
### Email-optional providers
|
||||
|
||||
By default, providers must return an email address. Set `email_optional` to `true` when creating or updating a provider to allow sign-in without an email. This applies to both OAuth2 and OIDC providers.
|
||||
|
||||
### OIDC-specific options
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------- |
|
||||
| `discovery_url` | `string` | `null` | Override the discovery document URL if the provider uses a non-standard location. |
|
||||
| `skip_nonce_check` | `bool` | `false` | Skip nonce validation on ID tokens. Use only for providers that do not support nonce. |
|
||||
|
||||
## Error reference
|
||||
|
||||
| Error code | HTTP status | Description |
|
||||
| ---------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
|
||||
| `validation_failed` | 400 | Invalid parameters: missing required fields, bad format, reserved params, or invalid URLs. |
|
||||
| `conflict` | 400 | A provider with the same identifier already exists. |
|
||||
| `over_custom_provider_quota` | 400 | Maximum number of custom providers reached. |
|
||||
| `custom_provider_not_found` | 404 | No provider exists with the given identifier. |
|
||||
@@ -25,7 +25,6 @@ Supabase Auth provides these [general configuration options](/dashboard/project/
|
||||
- **Allow new users to sign up**: Users will be able to sign up. If this config is disabled, only existing users can sign in.
|
||||
|
||||
- **Confirm Email**: Users will need to confirm their email address before signing in for the first time.
|
||||
|
||||
- Having **Confirm Email** disabled assumes that the user's email does not need to be verified in order to login and implicitly confirms the user's email in the database.
|
||||
- This option can be found in the email provider under the provider-specific configuration.
|
||||
{/* - If you previously relied on this config to autoconfirm a user's email address, you can switch to use **Allow unverified email sign in** instead. This new option allows the user to sign in with an unverified email which you can keep track of through the user object. It provides more versatility if you require your users to verify their email address in the future since you can structure your RLS policies to check the user's `email_verified` field. */}
|
||||
|
||||
@@ -97,7 +97,7 @@ With Deep Linking, you can configure this redirect to open a specific page. This
|
||||
|
||||
export default function Auth() {
|
||||
// Handle linking into app from email app.
|
||||
const url = Linking.useURL();
|
||||
const url = Linking.useLinkingURL();
|
||||
if (url) createSessionFromUrl(url);
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,7 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
|
||||
|
||||
## Rate limit behavior
|
||||
|
||||
Supabase Auth uses a token bucket algorithm for endpoint operations that are limited by IP address.
|
||||
Supabase Auth uses a token bucket algorithm for endpoint operations that are limited by IP address.
|
||||
|
||||
Each bucket has a maximum capacity of 30 requests. When the bucket is full, brief bursts of up to 30 requests can be allowed in a short period. Once the bucket empties, requests are rate limited until tokens refill. The rate limit defines the rate at which the bucket is refilled.
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ Supabase supports a suite of social providers. Follow these guides to configure
|
||||
</NavData>
|
||||
</div>
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Need to integrate with a provider not listed here? You can add any OAuth2 or OIDC-compatible provider using [Custom OAuth/OIDC Providers](/docs/guides/auth/custom-oauth-providers).
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Provider tokens
|
||||
|
||||
You can use the provider token and provider refresh token returned to make API calls to the OAuth provider. For example, you can use the Google provider token to access Google APIs on behalf of your user.
|
||||
|
||||
@@ -357,19 +357,16 @@ Development mode is sufficient for local development and testing. You only need
|
||||
Before your app can be used by the general public, you need to complete Facebook's App Review process:
|
||||
|
||||
1. **Complete App Settings**: In your Facebook app's **Settings > Basic**, fill in all required fields including:
|
||||
|
||||
- App Icon
|
||||
- Privacy Policy URL
|
||||
- Terms of Service URL (if applicable)
|
||||
- App Domain
|
||||
|
||||
2. **Request Permissions**: Navigate to **App Review > Permissions and Features** and request the permissions you need:
|
||||
|
||||
- `public_profile` - Usually pre-approved
|
||||
- `email` - Requires verification that your app needs email access
|
||||
|
||||
3. **Submit for Review**: Click **Submit for Review** and provide:
|
||||
|
||||
- Detailed instructions for how Facebook reviewers should test your login flow
|
||||
- A screencast video demonstrating the Facebook Login feature
|
||||
- Explanation of how user data will be used
|
||||
|
||||
@@ -416,7 +416,7 @@ language plpgsql
|
||||
security definer set search_path = ''
|
||||
as $$
|
||||
begin
|
||||
select 'hello world';
|
||||
return 'hello world';
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
@@ -53,7 +53,6 @@ You can use it in conjunction with Supabase by following these steps:
|
||||
```
|
||||
|
||||
Customize the source and Supabase database URL and options to fit your specific use case:
|
||||
|
||||
- `wal_buffers`: This parameter is set to '64MB' to allocate 64 megabytes of memory for write-ahead logging buffers. A larger value can help improve write performance by caching more data in memory before writing it to disk. This can be useful during data import operations to speed up the writing of transaction logs.
|
||||
- `max_wal_senders`: It is set to 0, to disable replication connections. This is done during the data import process to prevent replication-related conflicts and issues.
|
||||
- `statement_timeout`: The value is set to 0, which means it's disabled, allowing SQL statements to run without a time limit.
|
||||
|
||||
@@ -136,21 +136,18 @@ Before configuring BigQuery as a destination, set up the following in Google Clo
|
||||
1. **Google Cloud Platform (GCP) account**: [Sign up for GCP](https://cloud.google.com/gcp) if you don't have one
|
||||
|
||||
2. **BigQuery dataset**: Create a [BigQuery dataset](https://cloud.google.com/bigquery/docs/datasets-intro) in your GCP project
|
||||
|
||||
- Open the BigQuery console in GCP
|
||||
- Select your project
|
||||
- Click "Create Dataset"
|
||||
- Provide a dataset ID (e.g., `supabase_replication`)
|
||||
|
||||
3. **GCP service account key**: Create a [service account](https://cloud.google.com/iam/docs/keys-create-delete) with appropriate permissions:
|
||||
|
||||
- Go to IAM & Admin → Service Accounts
|
||||
- Click "Create Service Account"
|
||||
- Grant the **BigQuery Data Editor** role
|
||||
- Create and download the JSON key file
|
||||
|
||||
Required permissions:
|
||||
|
||||
- `bigquery.datasets.get`
|
||||
- `bigquery.tables.create`
|
||||
- `bigquery.tables.get`
|
||||
@@ -171,13 +168,11 @@ Before configuring BigQuery as a destination, set up the following in Google Clo
|
||||
/>
|
||||
|
||||
3. Configure the general settings:
|
||||
|
||||
- **Destination name**: A name to identify this destination (e.g., "BigQuery Warehouse")
|
||||
- **Publication**: The publication to replicate data from (created in [Step 1](#step-1-create-a-postgres-publication))
|
||||
- **Destination type**: Select **BigQuery**
|
||||
|
||||
4. Configure BigQuery-specific settings:
|
||||
|
||||
- **Project ID**: Your BigQuery project identifier (found in the GCP Console)
|
||||
- **Dataset ID**: The name of your BigQuery dataset (without the project ID)
|
||||
|
||||
@@ -190,7 +185,6 @@ Before configuring BigQuery as a destination, set up the following in Google Clo
|
||||
- **Service Account Key**: Your GCP service account key in JSON format (from Step 1)
|
||||
|
||||
5. Configure **Advanced Settings** (optional):
|
||||
|
||||
- **Batch wait time (milliseconds)**: How long to wait for more changes before sending a batch. Default is recommended for optimal performance.
|
||||
|
||||
6. Click **Create and start** to begin replication
|
||||
|
||||
@@ -6,71 +6,90 @@ description: 'Things to do before making your app publicly available'
|
||||
|
||||
After developing your project and deciding it's production ready, you should run through this checklist to ensure that your project:
|
||||
|
||||
- is secure
|
||||
- won't falter under the expected load
|
||||
- remains available whilst in production
|
||||
- [Is secure](#security)
|
||||
- [Won't falter under the expected load](#performance)
|
||||
- [Remains available whilst in production](#availability)
|
||||
|
||||
## Security
|
||||
|
||||
- Ensure RLS is enabled
|
||||
<Admonition type="tip">
|
||||
|
||||
Check and review issues in your database using [Security Advisor](/dashboard/project/_/database/security-advisor).
|
||||
|
||||
</Admonition>
|
||||
|
||||
- Ensure you have enabled row level security (RLS) on all tables from the [**Database > Tables**](/dashboard/project/_/database/tables) section of the Supabase Dashboard.
|
||||
- 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 Row Level Security (RLS) and setting row security policies:
|
||||
- Go to the Authentication > Policies page in the Supabase Dashboard to enable RLS and create security policies.
|
||||
- Go to the Database > Publications page in the Supabase Dashboard to manage replication tables.
|
||||
- Turn on [SSL Enforcement](/docs/guides/platform/ssl-enforcement) (see: [dashboard](/dashboard/project/_/auth/policies))
|
||||
- Enable [Network Restrictions](/docs/guides/platform/network-restrictions) for your database (see: [dashboard](/dashboard/project/_/database/settings#network-restrictions)).
|
||||
- Ensure that your Supabase Account is protected with multi-factor authentication (MFA).
|
||||
- If using a GitHub sign-in, [enable 2FA on GitHub](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). Since your GitHub account gives you administrative rights to your Supabase org, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
|
||||
- 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 > 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.
|
||||
- Ensure that you protect your Supabase Account with multi-factor authentication (MFA).
|
||||
- If using a GitHub sign-in, [enable 2FA on GitHub](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication).
|
||||
- Since your GitHub account gives you administrative rights to your Supabase org, you should protect it with a strong password and 2FA using a U2F key or a TOTP app.
|
||||
- If using email+password sign-in, set up [MFA for your Supabase account](/docs/guides/platform/multi-factor-authentication#enable-mfa).
|
||||
- Enable [MFA enforcement on your organization](/docs/guides/platform/network-restrictions). This ensures all users must have a valid MFA backed session to interact with organization and project resources.
|
||||
- Consider [adding multiple owners on your Supabase org](/dashboard/org/_/team). This ensures that if one of the owners is unreachable or loses access to their account, you still have Owner access to your org.
|
||||
- Ensure email confirmations are [enabled](/dashboard/project/_/auth/providers) in the `Settings > Auth` page.
|
||||
- Ensure that you've [set the expiry](/dashboard/project/_/auth/providers) for one-time passwords (OTPs) to a reasonable value that you are comfortable with. We recommend setting this to 3600 seconds (1 hour) or lower.
|
||||
- Increase the length of the OTP if you need a higher level of entropy.
|
||||
- Consider enabling [MFA enforcement on your organization](/docs/guides/platform/mfa/org-mfa-enforcement). This ensures all users must have a valid MFA-backed session to interact with organization and project resources.
|
||||
- Consider adding multiple owners on your Supabase org from [the **Organization > Team**](/dashboard/org/_/team) section of the Supabase Dashboard. This ensures that if one of the owners is unreachable or loses access to their account, you still have Owner access to your org.
|
||||
- Enable email confirmations in the [**Authentication > Providers**](/dashboard/project/_/auth/providers) section of the dashboard.
|
||||
- Set the expiry in the [**Authentication > Providers**](/dashboard/project/_/auth/providers) section of the dashboard for one-time passwords (OTPs) to a reasonable value that you are comfortable with.
|
||||
- We recommend setting this to 3600 seconds (1 hour) or lower.
|
||||
- Increase the length of the OTP if you need a higher level of entropy.
|
||||
- If your application requires a higher level of security, consider setting up [multi-factor authentication](/docs/guides/auth/auth-mfa) (MFA) for your users.
|
||||
- Use a custom SMTP server for auth emails so that your users can see that the mails are coming from a trusted domain (preferably the same domain that your app is hosted on). Grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc.
|
||||
- Consider how _you_ might abuse your service as an attacker, and take steps to mitigate it.
|
||||
- Review these [common cybersecurity threats](https://auth0.com/docs/security/prevent-threats).
|
||||
- Check and review issues in your database using [Security Advisor](/dashboard/project/_/database/security-advisor).
|
||||
|
||||
## Performance
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
Check and review issues in your database using [Performance Advisor](/dashboard/project/_/database/performance-advisor).
|
||||
|
||||
</Admonition>
|
||||
|
||||
- Ensure that you have suitable indices to cater to your common query patterns
|
||||
- [Learn more about indexes in Postgres](https://www.enterprisedb.com/postgres-tutorials/overview-postgresql-indexes).
|
||||
- `pg_stat_statements` can help you [identify hot or slow queries](https://www.virtual-dba.com/blog/postgresql-performance-identifying-hot-and-slow-queries/).
|
||||
- Perform load testing (preferably on a staging env)
|
||||
- Tools like [k6](https://k6.io/) can simulate traffic from many different users.
|
||||
- Upgrade your database if you require more resources. If you need anything beyond what is listed, contact enterprise@supabase.io.
|
||||
- Upgrade your database if you require more resources. If you need anything beyond what is listed in [the compute and disk table](/docs/guides/platform/compute-and-disk), contact [enterprise@supabase.io](mailto:enterprise@supabase.io).
|
||||
- If you are expecting a surge in traffic (for a big launch) and are on a Team or Enterprise Plan, [contact support](/dashboard/support/new) with more details about your launch and we'll help keep an eye on your project.
|
||||
- If you expect your database size to be > 4 GB, [enable](/dashboard/project/_/settings/addons?panel=pitr) the Point in Time Recovery (PITR) add-on. Daily backups can take up resources from your database when the backup is in progress. PITR is more resource efficient, since only the changes to the database are backed up.
|
||||
- Check and review issues in your database using [Performance Advisor](/dashboard/project/_/database/performance-advisor).
|
||||
- If you expect your database size to be > 4 GB, enable the Point in Time Recovery (PITR) add-on in the [**Settings > Add-ons**](/dashboard/project/_/settings/addons?panel=pitr) section of the dashboard.
|
||||
|
||||
## Availability
|
||||
|
||||
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Settings > Auth)
|
||||
- you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. You can refer to our [SMTP guide](/docs/guides/auth/auth-smtp) for more details.
|
||||
- The default rate limit for auth emails when using a custom SMTP provider is 30 new users per hour, if doing a major public announcement you will likely require more than this.
|
||||
- Applications on the Free Plan that exhibit extremely low activity in a 7 day period may be paused by Supabase to save on server resources.
|
||||
- You can restore paused projects from the Supabase dashboard.
|
||||
- Upgrade to Pro to guarantee that your project will not be paused for inactivity.
|
||||
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails in the [**Authentication > Emails > SMTP Settings**](/dashboard/project/_/auth/smtp) section of the dashboard.
|
||||
- You can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc. Read our [SMTP guide](/docs/guides/auth/auth-smtp) for more setup details.
|
||||
- The default rate limit for auth emails when using a custom SMTP provider is _30 new users per hour_. If you are doing a major public announcement, you will likely require more than this.
|
||||
- We may pause applications on the Free Plan that exhibit low activity in a 7-day period to save on server resources.
|
||||
- You can restore paused projects from [the Supabase dashboard](/dashboard/project/_).
|
||||
- Upgrade to Pro to guarantee that we won't pause your project for inactivity.
|
||||
- Database backups are not available for download for Free Plan projects.
|
||||
- You can set up your own backup systems using tools like [pg_dump](https://www.postgresql.org/docs/current/app-pgdump) or [wal-g](https://github.com/wal-g/wal-g).
|
||||
- Nightly backups for Pro Plan projects are available on the Supabase dashboard for up to 7 days.
|
||||
- Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. If a lower RPO is required, enable PITR.
|
||||
- Read [the Database Backups guide](/docs/guides/platform/backups) for more options and retention details.
|
||||
- If you need a lower recovery point objective (RPO), enable Point-in-Time Recovery (PITR). PITR allows you to back up a project at shorter intervals. This provides users an option to restore to any chosen point in time with second-level granularity.
|
||||
- Supabase Projects use disks that offer 99.8-99.9% durability by default.
|
||||
- Use Read Replicas if you require availability resilience to a disk failure event
|
||||
- Use [Read Replicas](/docs/guides/platform/read-replicas) if you require availability resilience to a disk failure event
|
||||
- Use PITR if you require durability resilience to a disk failure event
|
||||
- Upgrading to the Supabase Pro Plan will give you [access to our support team](/dashboard/support/new).
|
||||
- Upgrading to the Supabase Pro Plan gives you [access to our support team](/dashboard/support/new).
|
||||
|
||||
## Rate limiting, resource allocation, & abuse prevention
|
||||
|
||||
<Admonition type="caution" label="Shared Responsibility Model">
|
||||
|
||||
Running databases is a shared responsibility between you and Supabase. There are some things that we can take care of for you, and some things that you are responsible for.
|
||||
|
||||
Read more details in our [Shared Responsibility Model guide](/docs/guides/platform/shared-responsibility-model).
|
||||
|
||||
</Admonition>
|
||||
|
||||
- Supabase employs a number of safeguards against bursts of incoming traffic to prevent abuse and help maximize stability across the platform
|
||||
- If you're on a Team or Enterprise Plan and expect high load events, such as production launches, heavy load testing, or prolonged high resource usage, open a ticket via the [support form](https://supabase.help) for help. Provide at least 2 weeks notice.
|
||||
|
||||
### Auth rate limits
|
||||
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints. You can configure the auth rate limits for your project [here](/dashboard/project/_/auth/rate-limits).
|
||||
- The table below shows the rate limit quotas on the following authentication endpoints. You can configure the authentication rate limits for your project in the [**Authentication > Rate Limits**](/dashboard/project/_/auth/rate-limits) section of the dashboard.
|
||||
|
||||
| Endpoint | Path | Limited By | Rate Limit |
|
||||
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -87,41 +106,13 @@ After developing your project and deciding it's production ready, you should run
|
||||
### Realtime limits
|
||||
|
||||
- Review the [Realtime limits](/docs/guides/realtime/limits).
|
||||
- If you need limits increased you can always [contact support](/dashboard/support/new).
|
||||
- If you need limits increased, [contact support](/dashboard/support/new).
|
||||
|
||||
### Abuse prevention
|
||||
|
||||
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Refer to [our guide](/docs/guides/auth/auth-captcha) on how to protect against abuse using this method.
|
||||
- Supabase provides CAPTCHA protection on the signup, sign-in and password reset endpoints. Read [the Auth CAPTCHA guide](/docs/guides/auth/auth-captcha) for more details on how to protect against abuse using this method.
|
||||
|
||||
### Email link validity
|
||||
|
||||
- When working with enterprise systems, email scanners may scan and make a `GET` request to the reset password link or sign up link in your email. Since links in Supabase Auth are single use, a user who opens an email post-scan to click on a link will receive an error. To get around this problem,
|
||||
consider altering the email template to replace the original magic link with a link to a domain you control. The domain can present the user with a "Sign-in" button which redirect the user to the original magic link URL when clicked.
|
||||
|
||||
- When using a custom SMTP service, some services might have link tracking enabled which may overwrite or disform the email confirmation links sent by Supabase Auth. To prevent this from happening, we recommend that you disable link tracking when using a custom SMTP service.
|
||||
|
||||
## Subscribe to Supabase status page
|
||||
|
||||
Stay informed about Supabase service status by subscribing to the [Status Page](https://status.supabase.com/). We recommend setting up Slack notifications through an RSS feed to ensure your team receives timely updates about service status changes.
|
||||
|
||||
### Setting up Slack notifications
|
||||
|
||||
1. Install the RSS app in Slack:
|
||||
|
||||
- Visit the [RSS app page](https://slack.com/marketplace/A0F81R7U7-rss) in the Slack marketplace
|
||||
- Click `Add to Slack` if not already installed
|
||||
- Otherwise you will get straight to next step, no need to reinstall the app
|
||||
|
||||
2. Configure the Supabase status feed:
|
||||
|
||||
- Create a channel (e.g., `#supabase-status-alerts`) for status updates
|
||||
- On the [RSS app page](https://slack.com/marketplace/A0F81R7U7-rss) go to _Add a Feed_ section and set Feed URL to `https://status.supabase.com/history.rss`
|
||||
- Select your designated channel and click "Subscribe to this feed"
|
||||
|
||||
Once configured, your team will receive automatic notifications in Slack whenever the Supabase Status Page is updated.
|
||||
|
||||
For detailed setup instructions, see the [Add RSS feeds to Slack](https://slack.com/intl/en-nz/help/articles/218688467-Add-RSS-feeds-to-Slack).
|
||||
|
||||
## Next steps
|
||||
|
||||
This checklist is always growing so be sure to check back frequently, and also feel free to suggest additions and amendments by making a PR on [GitHub](https://github.com/supabase/supabase).
|
||||
- When working with enterprise systems, email scanners may scan and make a `GET` request to the reset password link or sign-up link in your email. Since links in Supabase Auth are single-use, a user who opens an email post-scan to click on a link will receive an error. To get around this problem, consider altering the email template to replace the original magic link with a link to a domain you control. The domain can present the user with a "Sign-in" button, which redirects the user to the original magic link URL when clicked.
|
||||
- When using a custom SMTP service, some services might have link tracking enabled which may overwrite or deform the email confirmation links sent by Supabase Auth. To prevent this from happening, we recommend that you disable link tracking when using a custom SMTP service.
|
||||
@@ -13,7 +13,6 @@ This guide explains the architecture and inner workings of Supabase Edge Functio
|
||||
To illustrate how edge functions operate, consider a photo-sharing app where users upload images and apply filters (e.g., grayscale or sepia) before saving them.
|
||||
|
||||
- **Workflow Overview**:
|
||||
|
||||
- A user uploads an original image to Supabase Storage.
|
||||
- When the user selects a filter, the client-side app (using the Supabase JavaScript SDK) invokes an edge function named something like "apply-filter."
|
||||
- The edge function:
|
||||
@@ -34,7 +33,6 @@ This example highlights edge functions as lightweight, on-demand code snippets t
|
||||
Deploying an edge function is straightforward and automated, requiring no manual server setup.
|
||||
|
||||
- **Steps to Deploy**:
|
||||
|
||||
1. Write the function code in your local Supabase project (e.g., in `supabase/functions/apply-filter/index.ts`).
|
||||
2. Run the command `supabase functions deploy apply-filter` via the Supabase CLI.
|
||||
3. The CLI bundles the function and its dependencies into an **ESZip file**—a compact format created by Deno that includes a complete module graph for quick loading and execution.
|
||||
@@ -52,7 +50,6 @@ Once deployed, the function is ready for invocation from anywhere, with Supabase
|
||||
Edge functions leverage a distributed architecture to minimize latency by running code close to the user.
|
||||
|
||||
- **Architecture Components**:
|
||||
|
||||
- **Global API Gateway**: Acts as the entry point for all requests. It uses the requester's IP address to determine geographic location and routes the request to the nearest edge location (e.g., routing a request from Amsterdam to Frankfurt).
|
||||
- **Edge Locations**: Supabase's network of data centers worldwide where functions are replicated. The ESZip bundle is automatically distributed to these locations upon deployment.
|
||||
- **Routing Logic**: Based on geolocation mapping, ensuring the function executes as close as possible to the user for optimal performance.
|
||||
@@ -68,20 +65,17 @@ This global edge network is what makes edge functions "edge-native," providing c
|
||||
The core of edge functions' efficiency lies in their execution environment, which prioritizes speed, isolation, and scalability.
|
||||
|
||||
- **Request Handling**:
|
||||
|
||||
1. A client sends an HTTP request (e.g., POST) to the function's URL, including parameters like auth headers, image ID, and filter type.
|
||||
2. The global API gateway routes it to the nearest edge location.
|
||||
3. At the edge, Supabase's **edge runtime** validates the request (e.g., checks authorization).
|
||||
|
||||
- **Execution Environment**:
|
||||
|
||||
- A new **V8 isolate** is spun up for each invocation. V8 is the JavaScript engine used by Chrome and Node.js, providing a lightweight, sandboxed environment.
|
||||
- Each isolate has its own memory heap and execution thread, ensuring complete isolation—no interference between concurrent requests.
|
||||
- The ESZip bundle is loaded into the isolate, and the function code runs.
|
||||
- After execution, the response (e.g., filtered image path) is sent back to the client.
|
||||
|
||||
- **Performance Optimizations**:
|
||||
|
||||
- **Cold Starts**: Even initial executions are fast (milliseconds) due to the compact ESZip format and minimal Deno runtime overhead.
|
||||
- **Warm Starts**: Isolates can remain active for a period (plan-dependent) to handle subsequent requests without restarting.
|
||||
- **Concurrency**: Multiple isolates can run simultaneously in the same edge location, supporting high traffic.
|
||||
@@ -95,7 +89,6 @@ Compared to traditional serverless or monolithic architectures, this setup offer
|
||||
## Benefits and use cases
|
||||
|
||||
- **Advantages**:
|
||||
|
||||
- **Low Latency**: Proximity to users reduces round-trip times.
|
||||
- **Scalability**: Handles variable loads without provisioning servers.
|
||||
- **Developer-Friendly**: Focus on code; Supabase manages the rest.
|
||||
|
||||
@@ -26,21 +26,19 @@ import { ImageResponse } from 'https://deno.land/x/og_edge@0.0.4/mod.ts'
|
||||
|
||||
export default function handler(req: Request) {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 128,
|
||||
background: 'lavender',
|
||||
}}
|
||||
>
|
||||
Hello OG Image!
|
||||
</div>
|
||||
)
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 128,
|
||||
background: 'lavender',
|
||||
}}
|
||||
>
|
||||
Hello OG Image!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Agent Skills
|
||||
---
|
||||
|
||||
Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. Agents are increasingly capable, but often don't have the context they need to do real work reliably. Skills solve this by giving agents access to procedural knowledge and company-, team-, and user-specific context they can load on demand. Agents with access to a set of skills can extend their capabilities based on the task they're working on.
|
||||
|
||||
## Installing skills
|
||||
|
||||
Install all Supabase skills using the skills CLI:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills
|
||||
```
|
||||
|
||||
To install a specific skill from the repository:
|
||||
|
||||
```bash
|
||||
npx skills add supabase/agent-skills --skill SKILL_NAME
|
||||
```
|
||||
|
||||
### Claude Code plugin
|
||||
|
||||
You can also install the skills as Claude Code plugins:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add supabase/agent-skills
|
||||
/plugin install postgres-best-practices@supabase-agent-skills
|
||||
```
|
||||
|
||||
Skills work with 18+ AI agents including Claude Code, GitHub Copilot, Cursor, Cline, and many others.
|
||||
|
||||
## Available skills
|
||||
|
||||
<AiSkillsIndex />
|
||||
|
||||
## Finding more skills
|
||||
|
||||
Browse the [skills.sh directory](https://skills.sh) to discover skills from the community. You can also search for skills using the CLI:
|
||||
|
||||
```bash
|
||||
npx skills find QUERY
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
- [Agent Skills Repository](https://github.com/supabase/agent-skills)
|
||||
- [Agent Skills Documentation](https://agentskills.io/home)
|
||||
- [Agent Skills Overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
|
||||
@@ -218,6 +218,7 @@ In addition to the Beta requirements, features in GA are covered by the [uptime
|
||||
| Platform | Read Replicas | `GA` | N/A |
|
||||
| Platform | Log Drains | `public alpha` | ✅ |
|
||||
| Platform | MCP | `public alpha` | ✅ |
|
||||
| Platform | PrivateLink | `beta` | N/A |
|
||||
| Studio | | `GA` | ✅ |
|
||||
| Studio | SSO | `GA` | ✅ |
|
||||
| Studio | Column Privileges | `public alpha` | ✅ |
|
||||
|
||||
@@ -9,7 +9,7 @@ description: 'Learn how to use Supabase in your Ionic Vue App.'
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/mhartington/supabase-ionic-vue).
|
||||
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/ionic-vue-user-management).
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -17,11 +17,11 @@ If you get stuck while working through this guide, refer to the [full example on
|
||||
|
||||
## Building the app
|
||||
|
||||
Let's start building the Vue app from scratch.
|
||||
Start by building the Vue app from scratch.
|
||||
|
||||
### Initialize an Ionic Vue app
|
||||
|
||||
We can use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize an app called `supabase-ionic-vue`:
|
||||
Use the [Ionic CLI](https://ionicframework.com/docs/cli) to initialize an app called `supabase-ionic-vue`:
|
||||
|
||||
```bash
|
||||
npm install -g @ionic/cli
|
||||
@@ -29,403 +29,66 @@ ionic start supabase-ionic-vue blank --type vue
|
||||
cd supabase-ionic-vue
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
Install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
And finally we want to save the environment variables in a `.env`.
|
||||
|
||||
All we need are the API URL and the key that you copied [earlier](#get-api-details).
|
||||
Save the environment variables in a `.env` file, including the API URL and key that you copied [earlier](#get-api-details).
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```bash name=.env
|
||||
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
|
||||
VUE_APP_SUPABASE_URL=YOUR_SUPABASE_URL
|
||||
VUE_APP_SUPABASE_KEY=YOUR_SUPABASE_KEY
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
With the API credentials in place, create a helper file to initialize the Supabase client. These variables will be exposed on the browser, and that's fine since Supabase enables [Row Level Security](/docs/guides/auth#row-level-security) on Databases by default.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```js name=src/supabase.ts
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string;
|
||||
const supabasePublishableKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY as string;
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabasePublishableKey);
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/supabase.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/supabase.ts"
|
||||
/>
|
||||
|
||||
### Set up a login route
|
||||
|
||||
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.
|
||||
Create a Vue component to manage logins and sign ups that uses Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=/src/views/Login.vue
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Login</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<div class="ion-padding">
|
||||
<h1>Supabase + Ionic Vue</h1>
|
||||
<p>Sign in via magic link with your email below</p>
|
||||
</div>
|
||||
<ion-list inset="true">
|
||||
<form @submit.prevent="handleLogin">
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Email</ion-label>
|
||||
<ion-input v-model="email" name="email" autocomplete type="email"></ion-input>
|
||||
</ion-item>
|
||||
<div class="ion-text-center">
|
||||
<ion-button type="submit" fill="clear">Login</ion-button>
|
||||
</div>
|
||||
</form>
|
||||
</ion-list>
|
||||
<p>{{ email }}</p>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { supabase } from '../supabase'
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
toastController,
|
||||
loadingController,
|
||||
} from '@ionic/vue'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LoginPage',
|
||||
components: {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonList,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
},
|
||||
setup() {
|
||||
const email = ref('')
|
||||
const handleLogin = async () => {
|
||||
const loader = await loadingController.create({})
|
||||
const toast = await toastController.create({ duration: 5000 })
|
||||
|
||||
try {
|
||||
await loader.present()
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
|
||||
|
||||
if (error) throw error
|
||||
|
||||
toast.message = 'Check your email for the login link!'
|
||||
await toast.present()
|
||||
} catch (error: any) {
|
||||
toast.message = error.error_description || error.message
|
||||
await toast.present()
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
return { handleLogin, email }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/views/Login.vue"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/views/Login.vue"
|
||||
/>
|
||||
|
||||
### Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
After a user has signed in, let them edit their profile details and manage their account with a new component called `Account.vue`.
|
||||
|
||||
Let's create a new component for that called `Account.vue`.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/views/Account.vue
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Account</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<form @submit.prevent="updateProfile">
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<p>Email</p>
|
||||
<p>{{ user?.email }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Name</ion-label>
|
||||
<ion-input type="text" v-model="profile.username" />
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Website</ion-label>
|
||||
<ion-input type="url" v-model="profile.website" />
|
||||
</ion-item>
|
||||
|
||||
<div class="ion-text-center">
|
||||
<ion-button type="submit" fill="clear">Update Profile</ion-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="ion-text-center">
|
||||
<ion-button fill="clear" @click="signOut">Log Out</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
toastController,
|
||||
loadingController,
|
||||
} from '@ionic/vue'
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { supabase } from '@/supabase'
|
||||
import type { User } from '@supabase/supabase-js'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AccountPage',
|
||||
components: {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonInput,
|
||||
IonButton,
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const user = ref<User | null>(null)
|
||||
|
||||
const profile = ref({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
})
|
||||
|
||||
const getProfile = async () => {
|
||||
const loader = await loadingController.create()
|
||||
const toast = await toastController.create({ duration: 5000 })
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select('username, website, avatar_url')
|
||||
.eq('id', user.value?.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) throw error
|
||||
|
||||
if (data) {
|
||||
profile.value = {
|
||||
username: data.username,
|
||||
website: data.website,
|
||||
avatar_url: data.avatar_url,
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.message = error.message
|
||||
await toast.present()
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async () => {
|
||||
const loader = await loadingController.create()
|
||||
const toast = await toastController.create({ duration: 5000 })
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const updates = {
|
||||
id: user.value?.id,
|
||||
...profile.value,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('profiles').upsert(updates, {
|
||||
returning: 'minimal',
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
} catch (error: any) {
|
||||
toast.message = error.message
|
||||
await toast.present()
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
const signOut = async () => {
|
||||
const loader = await loadingController.create()
|
||||
const toast = await toastController.create({ duration: 5000 })
|
||||
await loader.present()
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
toast.message = error.message
|
||||
await toast.present()
|
||||
} finally {
|
||||
await loader.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const loader = await loadingController.create()
|
||||
await loader.present()
|
||||
|
||||
const { data } = await supabase.auth.getSession()
|
||||
user.value = data.session?.user ?? null
|
||||
|
||||
if (!user.value) {
|
||||
router.push('/')
|
||||
} else {
|
||||
await getProfile()
|
||||
}
|
||||
|
||||
await loader.dismiss()
|
||||
})
|
||||
|
||||
return {
|
||||
user,
|
||||
profile,
|
||||
updateProfile,
|
||||
signOut,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/views/Account.vue"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/views/Account.vue"
|
||||
/>
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `App.vue` and our routes:
|
||||
With all the components in place, update `App.vue` and the app routes:
|
||||
|
||||
<$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/router/index.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/router/index.ts"
|
||||
/>
|
||||
|
||||
```ts name=src/router.index.ts
|
||||
import { createRouter, createWebHistory } from '@ionic/vue-router'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
import LoginPage from '../views/Login.vue'
|
||||
import AccountPage from '../views/Account.vue'
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Login',
|
||||
component: LoginPage,
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'Account',
|
||||
component: AccountPage,
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
```html name=src/App.vue
|
||||
<template>
|
||||
<ion-app>
|
||||
<ion-router-outlet />
|
||||
</ion-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IonApp, IonRouterOutlet, useIonRouter } from '@ionic/vue'
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
import { supabase } from './supabase'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
IonApp,
|
||||
IonRouterOutlet,
|
||||
},
|
||||
setup() {
|
||||
const router = useIonRouter()
|
||||
const user = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
supabase.auth
|
||||
.getSession()
|
||||
.then((resp) => {
|
||||
user.value = resp.data.session?.user ?? null
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Error fetching session', err)
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_event, session) => {
|
||||
user.value = session?.user ?? null
|
||||
})
|
||||
})
|
||||
|
||||
return { user }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/App.vue"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/App.vue"
|
||||
/>
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
|
||||
@@ -433,7 +96,7 @@ Once that's done, run this in a terminal window:
|
||||
ionic serve
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
And then open the browser to [localhost:8100](http://localhost:8100) and you should see the completed app.
|
||||
|
||||

|
||||
|
||||
@@ -443,179 +106,34 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
### Create an upload widget
|
||||
|
||||
First install two packages in order to interact with the user's camera.
|
||||
First install two packages to interact with the user's camera.
|
||||
|
||||
```bash
|
||||
npm install @ionic/pwa-elements @capacitor/camera
|
||||
```
|
||||
|
||||
[Capacitor](https://capacitorjs.com) is a cross-platform native runtime from Ionic that enables web apps to be deployed through the app store and provides access to native device API.
|
||||
[Capacitor](https://capacitorjs.com) is a cross-platform native runtime from Ionic that enables you to deploy web apps to app stores and provides access to native device API.
|
||||
|
||||
Ionic PWA elements is a companion package that will polyfill certain browser APIs that provide no user interface with custom Ionic UI.
|
||||
Ionic PWA elements is a companion package that polyfills certain browser APIs that provide no user interface with custom Ionic UI.
|
||||
|
||||
With those packages installed we can update our `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
|
||||
With those packages installed, update `main.ts` to include an additional bootstrapping call for the Ionic PWA Elements.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```ts name=src/main.tsx
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import { IonicVue } from '@ionic/vue'
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
import '@ionic/vue/css/ionic.bundle.css'
|
||||
|
||||
/* Theme variables */
|
||||
import './theme/variables.css'
|
||||
|
||||
import { defineCustomElements } from '@ionic/pwa-elements/loader'
|
||||
defineCustomElements(window)
|
||||
const app = createApp(App).use(IonicVue).use(router)
|
||||
|
||||
router.isReady().then(() => {
|
||||
app.mount('#app')
|
||||
})
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/main.ts"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/main.ts"
|
||||
/>
|
||||
|
||||
Then create an `AvatarComponent`.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/components/Avatar.vue
|
||||
<template>
|
||||
<div class="avatar">
|
||||
<div class="avatar_wrapper" @click="uploadAvatar">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" />
|
||||
<ion-icon v-else name="person" class="no-avatar"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, toRefs, watch, defineComponent } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
import { Camera, CameraResultType } from '@capacitor/camera'
|
||||
import { IonIcon } from '@ionic/vue'
|
||||
import { person } from 'ionicons/icons'
|
||||
export default defineComponent({
|
||||
name: 'AppAvatar',
|
||||
props: { path: String },
|
||||
emits: ['upload', 'update:path'],
|
||||
components: { IonIcon },
|
||||
setup(prop, { emit }) {
|
||||
const { path } = toRefs(prop)
|
||||
const avatarUrl = ref('')
|
||||
|
||||
const downloadImage = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage.from('avatars').download(path.value)
|
||||
if (error) throw error
|
||||
avatarUrl.value = URL.createObjectURL(data!)
|
||||
} catch (error: any) {
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
const photo = await Camera.getPhoto({
|
||||
resultType: CameraResultType.DataUrl,
|
||||
})
|
||||
if (photo.dataUrl) {
|
||||
const file = await fetch(photo.dataUrl)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` }))
|
||||
|
||||
const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}`
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, file)
|
||||
if (uploadError) {
|
||||
throw uploadError
|
||||
}
|
||||
emit('update:path', fileName)
|
||||
emit('upload')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(path, () => {
|
||||
if (path.value) downloadImage()
|
||||
})
|
||||
|
||||
return { avatarUrl, uploadAvatar, person }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.avatar {
|
||||
display: block;
|
||||
margin: auto;
|
||||
min-height: 150px;
|
||||
}
|
||||
.avatar .avatar_wrapper {
|
||||
margin: 16px auto 16px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
height: 150px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--ion-color-step-50);
|
||||
border: thick solid var(--ion-color-step-200);
|
||||
}
|
||||
.avatar .avatar_wrapper:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatar .avatar_wrapper ion-icon.no-avatar {
|
||||
width: 100%;
|
||||
height: 115%;
|
||||
}
|
||||
.avatar img {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
<$CodeSample
|
||||
path="/user-management/ionic-vue-user-management/src/components/Avatar.vue"
|
||||
lines={[[1, -1]]}
|
||||
meta="name=src/components/Avatar.vue"
|
||||
/>
|
||||
|
||||
### Add the new widget
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
```html name=src/views/Account.vue
|
||||
<template>
|
||||
<ion-page>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Account</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<avatar v-model:path="profile.avatar_url" @upload="updateProfile"></avatar>
|
||||
...
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import Avatar from '../components/Avatar.vue';
|
||||
export default defineComponent({
|
||||
name: 'AccountPage',
|
||||
components: {
|
||||
Avatar,
|
||||
....
|
||||
}
|
||||
|
||||
</script>
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
Add the widget to the Account page (already included in the Account.vue code above since the example includes the Avatar component by default).
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
@@ -44,7 +44,7 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
|
||||
|
||||
### App styling (optional)
|
||||
|
||||
An optional step is to update the CSS file `app/globals.css` to make the app look nice.
|
||||
An optional step is to update the CSS file `app/globals.css` to make the app look better.
|
||||
You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-user-management/app/globals.css).
|
||||
|
||||
### Supabase Server-Side Auth
|
||||
@@ -66,7 +66,7 @@ There are two different types of clients in Supabase:
|
||||
1. **Client Component client** - To access Supabase from Client Components, which run in the browser.
|
||||
2. **Server Component client** - To access Supabase from Server Components, Server Actions, and Route Handlers, which run only on the server.
|
||||
|
||||
It is recommended to create the following essential utilities files for creating clients, and organize them within `lib/supabase` at the root of the project.
|
||||
We recommend creating the following essential utilities files for creating clients, and organize them within `lib/supabase` at the root of the project.
|
||||
|
||||
Create a `client.ts` and a `server.ts` with the following functionalities for client-side Supabase and server-side Supabase, respectively.
|
||||
|
||||
@@ -88,9 +88,11 @@ meta="name=lib/supabase/server.ts"
|
||||
|
||||
### Next.js proxy
|
||||
|
||||
Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them. This is accomplished by:
|
||||
Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them.
|
||||
|
||||
- Refreshing the Auth token with the call to `supabase.auth.getUser`.
|
||||
You can accomplish this by:
|
||||
|
||||
- Refreshing the Auth token with the call to `supabase.auth.getClaims`.
|
||||
- Passing the refreshed Auth token to Server Components through `request.cookies.set`, so they don't attempt to refresh the same token themselves.
|
||||
- Passing the refreshed Auth token to the browser, so it replaces the old token. This is done with `response.cookies.set`.
|
||||
|
||||
@@ -100,15 +102,15 @@ You could also add a matcher, so that the Proxy only runs on routes that access
|
||||
|
||||
Be careful when protecting pages. The server gets the user session from the cookies, which anyone can spoof.
|
||||
|
||||
Always use `supabase.auth.getUser()` to protect pages and user data.
|
||||
Most of the time, use `supabase.auth.getClaims()` to protect pages and user data.
|
||||
|
||||
_Never_ trust `supabase.auth.getSession()` inside server code such as proxy. It isn't guaranteed to revalidate the Auth token.
|
||||
|
||||
It's safe to trust `getUser()` because it sends a request to the Supabase Auth server every time to revalidate the Auth token.
|
||||
It's safe to trust `getClaims()` because it validates the token in storage, either directly or by calling `getUser()` solely to check the result. It doesn't use the response from `getUser()` itself, only whether the validation succeeded.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. This is used by the `proxy.ts` file, which is a Next.js convention.
|
||||
Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. The `proxy.ts` file uses this, which is a Next.js convention.
|
||||
|
||||
<$CodeTabs>
|
||||
|
||||
@@ -184,9 +186,9 @@ Before proceeding, change the email template to support support a server-side au
|
||||
- Select the **Confirm signup** template.
|
||||
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
|
||||
|
||||
<Admonition type="tip">
|
||||
<Admonition type="tip" label="Did you know?">
|
||||
|
||||
**Did you know?** You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -162,16 +162,16 @@ meta="name=src/routes/+page.server.ts"
|
||||
|
||||
Change the email template to support a server-side authentication flow.
|
||||
|
||||
Before we proceed, let's change the email template to support sending a token hash:
|
||||
Before proceeding, change the email template to support sending a token hash:
|
||||
|
||||
- Go to the [**Auth** > **Emails**](/dashboard/project/_/auth/templates) page in the project dashboard.
|
||||
- Select the **Confirm signup** template.
|
||||
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
|
||||
- Repeat the previous step for **Magic link** template.
|
||||
|
||||
<Admonition type="tip">
|
||||
<Admonition type="tip" label="Did you know?">
|
||||
|
||||
**Did you know?** You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
You can also customize emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
|
||||
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -72,12 +72,28 @@ The Supabase CLI requires **Node.js 20 or later** when run via `npx` or `npm`. O
|
||||
|
||||
</Admonition>
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Installing the Supabase CLI globally using `npm install -g supabase` is **not supported**.
|
||||
|
||||
For global usage, install the CLI via Homebrew, Scoop, or the standalone binary.
|
||||
|
||||
Alternatively, you can run the CLI using `npx supabase` or install it locally as a dev dependency.
|
||||
|
||||
</Admonition>
|
||||
|
||||
You can also install the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase):
|
||||
|
||||
```sh
|
||||
npm install supabase --save-dev
|
||||
```
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Global installation using `npm install -g supabase` is not supported. For global CLI usage, install via [Homebrew](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=macos), [Scoop](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=windows), or the [standalone binary](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=linux).
|
||||
|
||||
</Admonition>
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -281,13 +281,11 @@ jobs:
|
||||
## Best practices
|
||||
|
||||
1. **Test Data Setup**
|
||||
|
||||
- Use begin and rollback to ensure test isolation
|
||||
- Create realistic test data that covers edge cases
|
||||
- Use different user roles and permissions in tests
|
||||
|
||||
2. **RLS Policy Testing**
|
||||
|
||||
- Test Create, Read, Update, Delete operations
|
||||
- Test with different user roles: anonymous and authenticated
|
||||
- Test edge cases and potential security bypasses
|
||||
@@ -310,7 +308,6 @@ For more complex, real-world examples of database testing, check out:
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Test Failures Due to RLS**
|
||||
|
||||
- Ensure you've set the correct role `set local role authenticated;`
|
||||
- Verify JWT claims are set `set local "request.jwt.claims"`
|
||||
- Check policy definitions match your test assumptions
|
||||
|
||||
@@ -67,13 +67,11 @@ create extension if not exists "basejump-supabase_test_helpers" version '0.0.6';
|
||||
The test helpers package provides several advantages over writing raw pgTAP tests:
|
||||
|
||||
1. **Simplified User Management**
|
||||
|
||||
- Create test users with `tests.create_supabase_user()`
|
||||
- Switch contexts with `tests.authenticate_as()`
|
||||
- Retrieve user IDs using `tests.get_supabase_uid()`
|
||||
|
||||
2. **Row Level Security (RLS) Testing Utilities**
|
||||
|
||||
- Verify RLS status with `tests.rls_enabled()`
|
||||
- Test policy enforcement
|
||||
- Simulate different user contexts
|
||||
@@ -262,7 +260,6 @@ This demo app implements:
|
||||
### What makes this complex?
|
||||
|
||||
1. **Layered Permissions**
|
||||
|
||||
- Role hierarchies affect access rights
|
||||
- Plan types influence user capabilities
|
||||
- Content state (draft/published) affects permissions
|
||||
|
||||
@@ -269,9 +269,15 @@ The table below shows the actions each role can take on the resources belonging
|
||||
| | Delete | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
|
||||
|
||||
[^1]: Available on the Team and Enterprise Plans.
|
||||
|
||||
[^2]: Sending anonymous data to OpenAI is opt in and can improve Studio AI Assistant's responses.
|
||||
|
||||
[^3]: Invites sent from a SSO account can only be accepted by another SSO account coming from the same identity provider. This is a security measure that prevents accidental invites to accounts not managed by your company's enterprise systems.
|
||||
|
||||
[^4]: Available on the Team and Enterprise Plans.
|
||||
|
||||
[^5]: Read-Only role is able to access secrets.
|
||||
|
||||
[^6]: Listed permissions are for the API and Dashboard.
|
||||
|
||||
[^7]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html).
|
||||
@@ -67,6 +67,7 @@ src="/docs/img/guides/platform/aws-marketplace-listing-subscribe.png"
|
||||
width={2270}
|
||||
height={632}
|
||||
/>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={4}>
|
||||
@@ -79,6 +80,7 @@ src="/docs/img/guides/platform/aws-marketplace-listing-success.png"
|
||||
width={1944}
|
||||
height={1254}
|
||||
/>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={5}>
|
||||
@@ -94,6 +96,7 @@ src={{
|
||||
width={3048}
|
||||
height={1058}
|
||||
/>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ The Fair Use Policy is generally applied to all projects of the restricted organ
|
||||
|
||||
To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted.
|
||||
|
||||
Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled).
|
||||
Restrictions due to usage limits are lifted once your quota refills at the start of the next billing cycle. Note that there may be a short delay after your billing period resets before restrictions are fully lifted. You can see when your current billing cycle ends on the [billing page](/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled).
|
||||
|
||||
## Reports and invoices
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ In paid organizations, Nano Compute are billed at the same price as Micro Comput
|
||||
| >16XL | - | [Contact Us](/dashboard/support/new?category=sales&subject=Enquiry%20about%20larger%20instance%20sizes) | Custom | Custom | Custom |
|
||||
|
||||
[^1]: Database max connections are recommended values and can be [customized via `max_connections`](/docs/guides/database/custom-postgres-config) depending on your use case. Be aware of [these considerations](/docs/guides/troubleshooting/how-to-change-max-database-connections-_BQ8P5) before modifying.
|
||||
|
||||
[^2]: Database size for each compute instance is the default recommendation but the actual performance of your database has many contributing factors, including resources available to it and the size of the data contained within it. See the [shared responsibility model](/docs/guides/platform/shared-responsibility-model) for more information.
|
||||
|
||||
[^3]: Compute resources on the Free plan are subject to change.
|
||||
|
||||
Compute sizes can be changed by first selecting your project in the dashboard [here](/dashboard/project/_/settings/compute-and-disk) and the upgrade process will [incur downtime](/docs/guides/platform/compute-and-disk#upgrades).
|
||||
|
||||
@@ -112,7 +112,6 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch
|
||||
1. To sign in your migrated users, use the Supabase Auth [sign in methods](/docs/reference/javascript/auth-signinwithpassword).
|
||||
|
||||
To check for edge cases where users aren't successfully migrated, use a fallback strategy. This ensures that users can continue to sign in seamlessly:
|
||||
|
||||
1. Try to sign in the user with Supabase Auth.
|
||||
1. If the signin fails, try to sign in with Auth0.
|
||||
1. If Auth0 signin succeeds, call the admin create user method again to create the user in Supabase Auth.
|
||||
|
||||
@@ -4,6 +4,8 @@ subtitle: 'Learn how to backup and restore projects using the Supabase CLI'
|
||||
breadcrumb: 'Migrations'
|
||||
---
|
||||
|
||||
# Migrating the database
|
||||
|
||||
## Backup database using the CLI
|
||||
|
||||
<StepHikeCompact>
|
||||
@@ -21,11 +23,11 @@ breadcrumb: 'Migrations'
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Get the new database connection string" fullWidth>
|
||||
On your project dashboard, click [Connect](/dashboard/project/_?showConnect=true).
|
||||
On your project dashboard, click [Connect](/dashboard/project/_?showConnect=true&method=session).
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Use the [Session pooler](/dashboard/project/_?showConnect=true&method=session) connection string by default. If your ISP supports IPv6 or you have the IPv4 add-on enabled, use the direct connection string.
|
||||
Use the [Session pooler](/dashboard/project/_?showConnect=true&method=session) connection string by default. If your network supports [IPv6](https://test-ipv6.com/) or you have the [IPv4 add-on](/docs/guides/platform/ipv4-address) enabled, use the direct connection string.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -65,7 +67,7 @@ breadcrumb: 'Migrations'
|
||||
```
|
||||
|
||||
```bash
|
||||
supabase db dump --db-url [CONNECTION_STRING] -f data.sql --use-copy --data-only
|
||||
supabase db dump --db-url [CONNECTION_STRING] -f data.sql --use-copy --data-only -x "storage.buckets_vectors" -x "storage.vector_indexes"
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
@@ -104,18 +106,17 @@ breadcrumb: 'Migrations'
|
||||
|
||||
- If Webhooks were used in the old database, enable [Database Webhooks](/dashboard/project/_/database/hooks).
|
||||
- If any non-default extensions were used in the old database, enable the [Extensions](/dashboard/project/_/database/extensions).
|
||||
- If Replication for Realtime was used in the old database, enable [Publication](/dashboard/project/_/database/publications) on the tables necessary
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Get the new database connection string" fullWidth>
|
||||
Go to the [project page](/dashboard/project/_/) and click the "**Connect**" button at the top of the page for the connection string.
|
||||
Go to [the **Connect** panel](/dashboard/project/_?showConnect=true&method=session) for the connection string.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Use the Session pooler connection string by default. If your ISP supports IPv6, use the direct connection string.
|
||||
Use the Session pooler connection string by default. If your ISP [supports IPv6](https://test-ipv6.com/), use the direct connection string.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -135,15 +136,14 @@ breadcrumb: 'Migrations'
|
||||
<StepHikeCompact.Step step={4}>
|
||||
|
||||
<StepHikeCompact.Details title="Get the database password" fullWidth>
|
||||
Reset the password in the [project connect page](/dashboard/project/_?showConnect=true).
|
||||
|
||||
Replace ```[YOUR-PASSWORD]``` in the connection string with the database password.
|
||||
Replace ```[YOUR-PASSWORD]``` in the connection string with the database password. If you do not remember your password, you can reset it on [the **Database > Settings**](/dashboard/project/_/database/settings) page of the Dashboard.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details title="Restore your Project with the CLI" fullWidth>
|
||||
<StepHikeCompact.Details title="Restore your Project with PSQL" fullWidth>
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
@@ -188,17 +188,18 @@ breadcrumb: 'Migrations'
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details title="Reactivate Database publications" fullWidth>
|
||||
If replication for Supabase Realtime was used in the old database, enable publication on [the **Database > Publications**](/dashboard/project/_/database/publications) section of the Dashboard on the tables necessary.
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Important project restoration notes
|
||||
## Special considerations
|
||||
|
||||
### Troubleshooting notes
|
||||
|
||||
- Setting the `session_replication_role` to `replica` disables all triggers so that columns are not double encrypted.
|
||||
- If you have created any [custom roles](/dashboard/project/_/database/roles) with `login` attribute, you have to manually set their passwords in the new project.
|
||||
- If you run into any permission errors related to `supabase_admin` during restore, edit the `schema.sql` file and comment out any lines containing `ALTER ... OWNER TO "supabase_admin"`.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -213,7 +214,7 @@ psql \
|
||||
--dbname "$NEW_DB_URL"
|
||||
```
|
||||
|
||||
### Schema changes to `auth` and `storage`
|
||||
#### Schema changes to `auth` and `storage`
|
||||
|
||||
If you have modified the `auth` and `storage` schemas in your old project, such as adding triggers or Row Level Security(RLS) policies, you have to restore them separately. The Supabase CLI can help you diff the changes to these schemas using the following commands.
|
||||
|
||||
@@ -222,61 +223,591 @@ supabase link --project-ref "$OLD_PROJECT_REF"
|
||||
supabase db diff --linked --schema auth,storage > changes.sql
|
||||
```
|
||||
|
||||
### Migrate storage objects
|
||||
## Troubleshooting notes
|
||||
|
||||
The new project has the old project's Storage buckets, but the Storage objects need to be migrated manually. Use this script to move storage objects from one project to another.
|
||||
#### Disabling triggers during restore:
|
||||
|
||||
```js
|
||||
// npm install @supabase/supabase-js@2
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
Setting `session_replication_role` to `replica` disables triggers during the migration, preventing columns from being double encrypted.
|
||||
|
||||
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
|
||||
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
|
||||
#### Custom roles require passwords
|
||||
|
||||
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
|
||||
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
|
||||
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:
|
||||
|
||||
;(async () => {
|
||||
const oldSupabaseRestClient = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY, {
|
||||
db: {
|
||||
schema: 'storage',
|
||||
},
|
||||
})
|
||||
const oldSupabaseClient = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY)
|
||||
const newSupabaseClient = createClient(NEW_PROJECT_URL, NEW_PROJECT_SERVICE_KEY)
|
||||
|
||||
// make sure you update max_rows in postgrest settings if you have a lot of objects
|
||||
// or paginate here
|
||||
const { data: oldObjects, error } = await oldSupabaseRestClient.from('objects').select()
|
||||
if (error) {
|
||||
console.log('error getting objects from old bucket')
|
||||
throw error
|
||||
}
|
||||
|
||||
for (const objectData of oldObjects) {
|
||||
console.log(`moving ${objectData.id}`)
|
||||
try {
|
||||
const { data, error: downloadObjectError } = await oldSupabaseClient.storage
|
||||
.from(objectData.bucket_id)
|
||||
.download(objectData.name)
|
||||
if (downloadObjectError) {
|
||||
throw downloadObjectError
|
||||
}
|
||||
|
||||
const { _, error: uploadObjectError } = await newSupabaseClient.storage
|
||||
.from(objectData.bucket_id)
|
||||
.upload(objectData.name, data, {
|
||||
upsert: true,
|
||||
contentType: objectData.metadata.mimetype,
|
||||
cacheControl: objectData.metadata.cacheControl,
|
||||
})
|
||||
if (uploadObjectError) {
|
||||
throw uploadObjectError
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error moving ', objectData)
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
})()
|
||||
```sql
|
||||
alter user "YOUR_USER" with password 'SOME_NEW_PASSWORD';
|
||||
```
|
||||
|
||||
#### `supabase_admin` permission errors
|
||||
|
||||
If you encounter permission errors related to `supabase_admin` during restore:
|
||||
|
||||
- Open `schema.sql`
|
||||
- Comment out any lines containing:
|
||||
|
||||
```sql
|
||||
ALTER ... OWNER TO "supabase_admin"
|
||||
```
|
||||
|
||||
#### `cli_login_postgres` role grant error
|
||||
|
||||
If you encounter the error:
|
||||
|
||||
```sh
|
||||
ERROR: permission denied to grant role "postgres"
|
||||
DETAIL: Only roles with the ADMIN option on role "postgres" may grant this role.
|
||||
```
|
||||
|
||||
- Open `roles.sql`
|
||||
- Comment out the line:
|
||||
|
||||
```sql
|
||||
GRANT "postgres" TO "cli_login_postgres" WITH INHERIT FALSE GRANTED BY "supabase_admin";
|
||||
```
|
||||
|
||||
#### `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:
|
||||
|
||||
```sh
|
||||
"message":"Failed to create login role:
|
||||
ERROR: 0LP01: role "postgres" is a member of role "cli_login_postgres"
|
||||
```
|
||||
|
||||
To resolve the issue, drop the custom `cli_login_postgres` role. Then the CLI can recreate it with the right privileges:
|
||||
|
||||
```sql
|
||||
DROP ROLE IF EXISTS cli_login_postgres;
|
||||
```
|
||||
|
||||
# Migrating edge functions
|
||||
|
||||
## Steps (using the Supabase CLI):
|
||||
|
||||
<StepHikeCompact>
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="Login to your Supabase Account" fullWidth>
|
||||
With the Supabase CLI [Supabase CLI](/docs/guides/local-development/cli/getting-started), run:
|
||||
```bash
|
||||
supabase login
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="List your edge functions" fullWidth>
|
||||
```bash
|
||||
supabase functions list --project-ref your_project_ref
|
||||
```
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Download your functions" fullWidth>
|
||||
You can download an individual function with the following command:
|
||||
```bash
|
||||
supabase functions download YOUR_FUNCTION_NAME --project-ref your_project_ref
|
||||
```
|
||||
<Admonition type="note">
|
||||
|
||||
The command will not download [import maps](/docs/guides/functions/dependencies#using-import-maps-legacy) nor [deno.json](/docs/guides/functions/dependencies#using-denojson-recommended) files. If your edge functions rely on them for dependency management, you will have to add them back manually.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Deploy the functions" fullWidth>
|
||||
```bash
|
||||
supabase functions deploy --project-ref your_target_project_ref
|
||||
```
|
||||
This deploys all functions within the `supabase/functions` to the target project. You can confirm by checking your Edge Functions on [the project dashboard](/dashboard/project/_/functions)
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
## Steps (using the Supabase Dashboard):
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Dependencies defined through [import maps](/docs/guides/functions/dependencies#using-import-maps-legacy) and [deno.json](/docs/guides/functions/dependencies#using-denojson-recommended) files will need to be rewritten to rely on their [direct import paths](/docs/guides/functions/dependencies#importing-dependencies) when using this approach.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<StepHikeCompact>
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
In the source project, navigate to **Edge Functions** from the side menu
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
Using the `Download` button, download your desired function as zip: 
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
In the target project, navigate to **Edge Functions** from the side menu
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
Click on the `Deploy a new function` button, select **Via Editor** operation
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={5}>
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
Drag and drop your downloaded function (the zip function from step 2) into the editor
|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
Add your function name and click on the `Deploy function` button to deploy the function:
|
||||

|
||||
</StepHikeCompact.Details>
|
||||
</StepHikeCompact.Step>
|
||||
</StepHikeCompact>
|
||||
|
||||
# Migrating storage objects
|
||||
|
||||
<StepHikeCompact>
|
||||
<StepHikeCompact.Step step={1}>
|
||||
<StepHikeCompact.Details title="On your machine, create a javascript repository">
|
||||
Using your preferred JavaScript package manager, create a new project with the `supabase` client package
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm_initiate"
|
||||
queryGroup="initiate"
|
||||
>
|
||||
<TabPanel id="npm_initiate" label="npm">
|
||||
```bash
|
||||
npm init -y
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
</TabPanel>
|
||||
<TabPanel id="pnpm_initiate" label="pnpm">
|
||||
```bash
|
||||
pnpm init -y
|
||||
pnpm install @supabase/supabase-js
|
||||
```
|
||||
</TabPanel>
|
||||
<TabPanel id="yarn_initiate" label="yarn">
|
||||
```bash
|
||||
yarn init -y
|
||||
yarn add @supabase/supabase-js
|
||||
```
|
||||
</TabPanel>
|
||||
<TabPanel id="bun_initiate" label="bun">
|
||||
```bash
|
||||
bun init -y
|
||||
bun install @supabase/supabase-js
|
||||
```
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
<StepHikeCompact.Details title="Create an index.js file in your Node.js project">
|
||||
Add the example script to it.
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
```js name=index.js
|
||||
// npm install @supabase/supabase-js@2
|
||||
const { createClient } = require('@supabase/supabase-js')
|
||||
|
||||
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
|
||||
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
|
||||
|
||||
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
|
||||
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
|
||||
|
||||
const oldSupabase = createClient(OLD_PROJECT_URL, OLD_PROJECT_SERVICE_KEY)
|
||||
const newSupabase = createClient(NEW_PROJECT_URL, NEW_PROJECT_SERVICE_KEY)
|
||||
|
||||
function createLoadingAnimation(message) {
|
||||
const readline = require('readline')
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
let i = 0
|
||||
let timer
|
||||
let stopped = false
|
||||
|
||||
const animate = () => {
|
||||
if (stopped) return
|
||||
process.stdout.write(`\r${frames[i]} ${message}`)
|
||||
i = (i + 1) % frames.length
|
||||
timer = setTimeout(animate, 80)
|
||||
}
|
||||
|
||||
animate()
|
||||
|
||||
return {
|
||||
stop: (finalMessage = '') => {
|
||||
stopped = true
|
||||
clearTimeout(timer)
|
||||
readline.clearLine(process.stdout, 0)
|
||||
readline.cursorTo(process.stdout, 0)
|
||||
process.stdout.write(`✓ ${finalMessage || message}\n`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all files in a bucket, handling nested folders recursively.
|
||||
*/
|
||||
async function listAllFiles(bucket, path = '') {
|
||||
const loader = createLoadingAnimation(`Listing files in '${bucket}${path ? '/' + path : ''}'...`)
|
||||
|
||||
try {
|
||||
const { data, error } = await oldSupabase.storage.from(bucket).list(path, { limit: 1000 })
|
||||
if (error) {
|
||||
loader.stop(`Error listing files in '${bucket}${path ? '/' + path : ''}'`)
|
||||
throw new Error(`❌ Error listing files in bucket '${bucket}': ${error.message}`)
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
loader.stop(`No files found in '${bucket}${path ? '/' + path : ''}'`)
|
||||
return []
|
||||
}
|
||||
|
||||
let files = []
|
||||
for (const item of data) {
|
||||
if (!item.metadata) {
|
||||
loader.stop(`Found folder '${item.name}' in '${bucket}${path ? '/' + path : ''}'`)
|
||||
const subFiles = await listAllFiles(bucket, `${path}${item.name}/`)
|
||||
files = files.concat(subFiles)
|
||||
} else {
|
||||
files.push({ fullPath: `${path}${item.name}`, metadata: item.metadata })
|
||||
}
|
||||
}
|
||||
|
||||
loader.stop(`Found ${files.length} files in '${bucket}${path ? '/' + path : ''}'`)
|
||||
return files
|
||||
} catch (error) {
|
||||
loader.stop()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bucket in the new Supabase project if it doesn't exist.
|
||||
*/
|
||||
async function ensureBucketExists(bucketName, options = {}) {
|
||||
const { data: existingBucket, error: getBucketError } =
|
||||
await newSupabase.storage.getBucket(bucketName)
|
||||
|
||||
if (getBucketError && !getBucketError.message.includes('not found')) {
|
||||
throw new Error(`❌ Error checking if bucket '${bucketName}' exists: ${getBucketError.message}`)
|
||||
}
|
||||
|
||||
if (!existingBucket) {
|
||||
console.log(`🪣 Creating bucket '${bucketName}' in new project...`)
|
||||
const { error } = await newSupabase.storage.createBucket(bucketName, options)
|
||||
if (error) throw new Error(`❌ Failed to create bucket '${bucketName}': ${error.message}`)
|
||||
console.log(`✅ Created bucket '${bucketName}'`)
|
||||
} else {
|
||||
console.log(`ℹ️ Bucket '${bucketName}' already exists in new project`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a single file from the old project to the new one.
|
||||
*/
|
||||
async function migrateFile(sourceBucketName, targetBucketName, file) {
|
||||
const loader = createLoadingAnimation(
|
||||
`Migrating ${file.fullPath} in bucket '${sourceBucketName}' to '${targetBucketName}'...`
|
||||
)
|
||||
|
||||
try {
|
||||
const { data, error: downloadError } = await oldSupabase.storage
|
||||
.from(sourceBucketName)
|
||||
.download(file.fullPath)
|
||||
if (downloadError) {
|
||||
loader.stop(`Failed to migrate ${file.fullPath}: Download error`)
|
||||
throw new Error(`Download failed: ${downloadError.message}`)
|
||||
}
|
||||
|
||||
// Preserve all available metadata from the original file
|
||||
const uploadOptions = {
|
||||
upsert: true,
|
||||
contentType: file.metadata?.mimetype,
|
||||
cacheControl: file.metadata?.cacheControl,
|
||||
}
|
||||
|
||||
const { error: uploadError } = await newSupabase.storage
|
||||
.from(targetBucketName)
|
||||
.upload(file.fullPath, data, uploadOptions)
|
||||
if (uploadError) {
|
||||
loader.stop(`Failed to migrate ${file.fullPath}: Upload error`)
|
||||
throw new Error(`Upload failed: ${uploadError.message}`)
|
||||
}
|
||||
|
||||
loader.stop(
|
||||
`Migrated ${file.fullPath} in bucket '${sourceBucketName}' to '${targetBucketName}'`
|
||||
)
|
||||
return { success: true, path: file.fullPath }
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`❌ Error migrating ${file.fullPath} in bucket '${targetBucketName}':`,
|
||||
err.message
|
||||
)
|
||||
return { success: false, path: file.fullPath, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
function chunkArray(array, size) {
|
||||
const chunks = []
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunks.push(array.slice(i, i + size))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates all buckets and files from the old Supabase project to the new one.
|
||||
* Processes files in parallel within batches for efficiency.
|
||||
*/
|
||||
async function migrateBuckets() {
|
||||
console.log('🔄 Starting Supabase Storage migration...')
|
||||
console.log(`📦 Source project: ${OLD_PROJECT_URL}`)
|
||||
console.log(`📦 Target project: ${NEW_PROJECT_URL}`)
|
||||
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
|
||||
console.log(
|
||||
'\n⚠️ WARNING: This migration may overwrite files in the target project if they have the same paths.'
|
||||
)
|
||||
console.log('⚠️ It is recommended to back up your target project before proceeding.')
|
||||
|
||||
const answer = await new Promise((resolve) => {
|
||||
readline.question('Do you want to proceed with the migration? (yes/no): ', resolve)
|
||||
})
|
||||
|
||||
readline.close()
|
||||
|
||||
if (answer.toLowerCase() !== 'yes') {
|
||||
console.log('Migration canceled by user.')
|
||||
return { canceled: true }
|
||||
}
|
||||
|
||||
console.log('\n📦 Fetching all buckets from old project...')
|
||||
|
||||
const { data: oldBuckets, error: bucketListError } = await oldSupabase.storage.listBuckets()
|
||||
|
||||
if (bucketListError) throw new Error(`❌ Error fetching buckets: ${bucketListError.message}`)
|
||||
console.log(`✅ Found ${oldBuckets.length} buckets to migrate.`)
|
||||
|
||||
const { data: existingBuckets, error: existingBucketsError } =
|
||||
await newSupabase.storage.listBuckets()
|
||||
if (existingBucketsError)
|
||||
throw new Error(`❌ Error fetching existing buckets: ${existingBucketsError.message}`)
|
||||
|
||||
const existingBucketNames = existingBuckets.map((b) => b.name)
|
||||
const conflictingBuckets = oldBuckets.filter((b) => existingBucketNames.includes(b.name))
|
||||
|
||||
let conflictStrategy = 2
|
||||
|
||||
if (conflictingBuckets.length > 0) {
|
||||
console.log('\n⚠️ The following buckets already exist in the target project:')
|
||||
conflictingBuckets.forEach((b) => console.log(` - ${b.name}`))
|
||||
|
||||
const conflictAnswer = await new Promise((resolve) => {
|
||||
const rl = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
rl.question(
|
||||
'\nHow do you want to handle existing buckets?\n' +
|
||||
'1. Skip existing buckets\n' +
|
||||
'2. Merge files (may overwrite existing files)\n' +
|
||||
'3. Rename buckets in target (add suffix "_migrated")\n' +
|
||||
'4. Cancel migration\n' +
|
||||
'Enter your choice (1-4): ',
|
||||
(answer) => {
|
||||
rl.close()
|
||||
resolve(answer)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (conflictAnswer === '4') {
|
||||
console.log('Migration canceled by user.')
|
||||
return { canceled: true }
|
||||
}
|
||||
|
||||
conflictStrategy = parseInt(conflictAnswer)
|
||||
if (isNaN(conflictStrategy) || conflictStrategy < 1 || conflictStrategy > 3) {
|
||||
console.log('Invalid choice. Migration canceled.')
|
||||
return { canceled: true }
|
||||
}
|
||||
}
|
||||
|
||||
const migrationStats = {
|
||||
totalBuckets: oldBuckets.length,
|
||||
processedBuckets: 0,
|
||||
skippedBuckets: 0,
|
||||
totalFiles: 0,
|
||||
successfulFiles: 0,
|
||||
failedFiles: 0,
|
||||
failedFilesList: [],
|
||||
}
|
||||
|
||||
for (const bucket of oldBuckets) {
|
||||
const bucketName = bucket.name
|
||||
console.log(`\n📁 Processing bucket: ${bucketName}`)
|
||||
|
||||
let targetBucketName = bucketName
|
||||
|
||||
if (existingBucketNames.includes(bucketName)) {
|
||||
if (conflictStrategy === 1) {
|
||||
console.log(`⏩ Skipping bucket '${bucketName}' as it already exists in target project`)
|
||||
migrationStats.skippedBuckets++
|
||||
continue
|
||||
} else if (conflictStrategy === 3) {
|
||||
targetBucketName = `${bucketName}_migrated`
|
||||
console.log(`🔄 Renaming bucket to '${targetBucketName}' in target project`)
|
||||
} else {
|
||||
console.log(`🔄 Merging files into existing bucket '${bucketName}' in target project`)
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve bucket configuration when creating in the new project
|
||||
if (targetBucketName !== bucketName || !existingBucketNames.includes(bucketName)) {
|
||||
await ensureBucketExists(targetBucketName, {
|
||||
public: bucket.public,
|
||||
fileSizeLimit: bucket.file_size_limit,
|
||||
allowedMimeTypes: bucket.allowed_mime_types,
|
||||
})
|
||||
}
|
||||
|
||||
const files = await listAllFiles(bucketName)
|
||||
console.log(`✅ Found ${files.length} files in bucket '${bucketName}'.`)
|
||||
migrationStats.totalFiles += files.length
|
||||
|
||||
const batches = chunkArray(files, 10)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
console.log(`\n🚀 Processing batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
|
||||
|
||||
const results = await Promise.all(
|
||||
batches[i].map((file) => migrateFile(bucketName, targetBucketName, file))
|
||||
)
|
||||
|
||||
const batchSuccesses = results.filter((r) => r.success).length
|
||||
const batchFailures = results.filter((r) => !r.success)
|
||||
|
||||
migrationStats.successfulFiles += batchSuccesses
|
||||
migrationStats.failedFiles += batchFailures.length
|
||||
migrationStats.failedFilesList.push(...batchFailures.map((f) => f.path))
|
||||
|
||||
console.log(
|
||||
`✅ Completed batch ${i + 1}/${batches.length}: ${batchSuccesses} succeeded, ${batchFailures.length} failed`
|
||||
)
|
||||
}
|
||||
|
||||
migrationStats.processedBuckets++
|
||||
console.log(`✅ Completed bucket '${bucketName}' migration`)
|
||||
}
|
||||
|
||||
console.log('\n📊 Migration Summary:')
|
||||
console.log(
|
||||
`Buckets: ${migrationStats.processedBuckets}/${migrationStats.totalBuckets} processed, ${migrationStats.skippedBuckets} skipped`
|
||||
)
|
||||
console.log(
|
||||
`Files: ${migrationStats.successfulFiles} succeeded, ${migrationStats.failedFiles} failed (${migrationStats.totalFiles} total)`
|
||||
)
|
||||
|
||||
if (migrationStats.failedFiles > 0) {
|
||||
console.log('\n⚠️ Failed files:')
|
||||
migrationStats.failedFilesList.forEach((path) => console.log(` - ${path}`))
|
||||
return migrationStats
|
||||
}
|
||||
|
||||
return migrationStats
|
||||
}
|
||||
|
||||
migrateBuckets()
|
||||
.then((stats) => {
|
||||
if (stats.failedFiles > 0) {
|
||||
console.log(`\n⚠️ Migration completed with ${stats.failedFiles} failed files.`)
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log('\n🎉 Migration completed successfully!')
|
||||
process.exit(0)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('❌ Fatal error during migration:', err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
<StepHikeCompact.Details title="Add the relevant project variables to the script">
|
||||
Get the [secret keys](/dashboard/project/_/settings/api-keys) or [service_role keys](/dashboard/project/_/settings/api-keys/legacy) for both your new and old projects, then substitute them into the script.
|
||||
From the [Data API settings](/dashboard/project/_/integrations/data_api/overview), copy your project URL and add it to the script as well.
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
```js name='index.js'
|
||||
//rest of code
|
||||
...
|
||||
|
||||
// add relevant details for old project
|
||||
const OLD_PROJECT_URL = 'https://xxx.supabase.co'
|
||||
const OLD_PROJECT_SERVICE_KEY = 'old-project-service-key-xxx'
|
||||
|
||||
// add relevant details for new project
|
||||
const NEW_PROJECT_URL = 'https://yyy.supabase.co'
|
||||
const NEW_PROJECT_SERVICE_KEY = 'new-project-service-key-yyy'
|
||||
|
||||
...
|
||||
//rest of code
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
<StepHikeCompact.Step step={4}>
|
||||
<StepHikeCompact.Details title="Run the script from your command line">
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
<StepHikeCompact.Code>
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="npm_run"
|
||||
queryGroup="npm_run"
|
||||
>
|
||||
<TabPanel id="npm_run" label="node">
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
</TabPanel>
|
||||
<TabPanel id="bun_run" label="bun">
|
||||
```bash
|
||||
bun index.js
|
||||
```
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</StepHikeCompact.Code>
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
## Resources
|
||||
|
||||
- [Connecting with PSQL](/docs/guides/database/psql)
|
||||
@@ -16,15 +16,12 @@ Projects must meet these requirements to use Read Replicas:
|
||||
|
||||
1. Running on AWS.
|
||||
2. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons).
|
||||
|
||||
- Read Replicas are started on the same compute instance as the Primary to keep up with changes.
|
||||
|
||||
3. Running on Postgres 15+.
|
||||
|
||||
- For projects running on older versions of Postgres, you need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
|
||||
|
||||
4. Not using [legacy logical backups](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
|
||||
- Physical backups are automatically enabled if using [Point in time recovery (PITR)](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
|
||||
## Creating a Read Replica
|
||||
|
||||
@@ -19,10 +19,12 @@ Queues couples the reliability of Postgres with the simplicity Supabase's platfo
|
||||
<br />A Message is delivered exactly once to a consumer within a customizable visibility window.
|
||||
- **Message Durability and Archival**
|
||||
<br />
|
||||
Messages are stored in Postgres and you can choose to archive them for analytical or auditing purposes.
|
||||
Messages are stored in Postgres and you can choose to archive them for analytical or auditing
|
||||
purposes.
|
||||
- **Granular Authorization**
|
||||
<br />
|
||||
Control client-side consumer access to Queues with API permissions and Row Level Security (RLS) policies.
|
||||
Control client-side consumer access to Queues with API permissions and Row Level Security (RLS)
|
||||
policies.
|
||||
- **Queue Management and Monitoring**
|
||||
<br />
|
||||
Create, manage, and monitor Queues and Messages in the Supabase Dashboard.
|
||||
|
||||
@@ -60,7 +60,7 @@ height={625}
|
||||
| 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](/docs/guides/platform/pricing) |
|
||||
| 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) |
|
||||
|
||||
@@ -100,15 +100,15 @@ This should return the EC public key (the symmetric key is excluded). Third-part
|
||||
|
||||
New variables default to empty values in `.env.example`. When empty, the API gateway and all services operate in legacy-only mode: `sb_publishable` and `sb_secret` API keys are not configured.
|
||||
|
||||
| Environment variable (existing and new) | Type | Description |
|
||||
|---|---|---|
|
||||
| `JWT_SECRET` | Symmetric secret | **Existing:** Shared secret for signing and verifying HS256 JWTs. Used by multiple services. |
|
||||
| `ANON_KEY` | HS256 JWT | **Existing:** Legacy client-side API key. Embedded JWT with `role: "anon"`. |
|
||||
| `SERVICE_ROLE_KEY` | HS256 JWT | **Existing:** Legacy server-side API key. Embedded JWT with `role: "service_role"`. |
|
||||
| `SUPABASE_PUBLISHABLE_KEY` | Opaque | **New:** Short random key with checksum. Replaces `ANON_KEY` for **client-side** use. |
|
||||
| `SUPABASE_SECRET_KEY` | Opaque | **New:** Short random key with checksum. Replaces `SERVICE_ROLE_KEY` for **server-side** use. |
|
||||
| `JWT_KEYS` | JSON array | **New:** JSON array of signing JWKs containing the new asymmetric key pair and the legacy symmetric key. Used by Auth to sign tokens. |
|
||||
| `JWT_JWKS` | JWKS (JSON) | **New:** Contains the new public key and the legacy symmetric key. Used by PostgREST, Realtime, and Storage to verify tokens. |
|
||||
| Environment variable (existing and new) | Type | Description |
|
||||
| --------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `JWT_SECRET` | Symmetric secret | **Existing:** Shared secret for signing and verifying HS256 JWTs. Used by multiple services. |
|
||||
| `ANON_KEY` | HS256 JWT | **Existing:** Legacy client-side API key. Embedded JWT with `role: "anon"`. |
|
||||
| `SERVICE_ROLE_KEY` | HS256 JWT | **Existing:** Legacy server-side API key. Embedded JWT with `role: "service_role"`. |
|
||||
| `SUPABASE_PUBLISHABLE_KEY` | Opaque | **New:** Short random key with checksum. Replaces `ANON_KEY` for **client-side** use. |
|
||||
| `SUPABASE_SECRET_KEY` | Opaque | **New:** Short random key with checksum. Replaces `SERVICE_ROLE_KEY` for **server-side** use. |
|
||||
| `JWT_KEYS` | JSON array | **New:** JSON array of signing JWKs containing the new asymmetric key pair and the legacy symmetric key. Used by Auth to sign tokens. |
|
||||
| `JWT_JWKS` | JWKS (JSON) | **New:** Contains the new public key and the legacy symmetric key. Used by PostgREST, Realtime, and Storage to verify tokens. |
|
||||
|
||||
### Differences from the Supabase platform
|
||||
|
||||
@@ -186,7 +186,6 @@ For **Realtime WebSocket** connections, the API key is sent as a `?apikey=` quer
|
||||
|
||||
**Storage** and **Edge Functions** accept requests without an API key. These services handle their own authentication.
|
||||
|
||||
|
||||
### API gateway routing
|
||||
|
||||
Kong is configured with two consumers that each accept both the legacy and new API keys:
|
||||
@@ -195,27 +194,27 @@ Kong is configured with two consumers that each accept both the legacy and new A
|
||||
consumers:
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_ANON_KEY # legacy HS256 JWT (ANON_KEY)
|
||||
- key: $SUPABASE_PUBLISHABLE_KEY # new opaque key (omitted when not configured)
|
||||
- key: $SUPABASE_ANON_KEY # legacy HS256 JWT (ANON_KEY)
|
||||
- key: $SUPABASE_PUBLISHABLE_KEY # new opaque key (omitted when not configured)
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_SERVICE_KEY # legacy HS256 JWT (SERVICE_ROLE_KEY)
|
||||
- key: $SUPABASE_SECRET_KEY # new opaque key (omitted when not configured)
|
||||
- key: $SUPABASE_SERVICE_KEY # legacy HS256 JWT (SERVICE_ROLE_KEY)
|
||||
- key: $SUPABASE_SECRET_KEY # new opaque key (omitted when not configured)
|
||||
```
|
||||
|
||||
When new API keys have not been added yet, the `kong-entrypoint.sh` script removes the empty credential entries before Kong loads the config.
|
||||
|
||||
To assist with the authorization flows a specialized configuration in `kong.yml` substitutes internal, gateway-level-only pre-signed JWTs for `sb_publishable` and `sb_secret` API keys. These pre-signed JWTs are also auto-configured in `.env` but **should not** be used in any application code.
|
||||
|
||||
| Route | Service | API key required | Header substitution |
|
||||
|---|---|---|---|
|
||||
| `/auth/v1/*` | Auth | Yes | `Authorization` |
|
||||
| `/rest/v1/*` | PostgREST | Yes | `Authorization` |
|
||||
| `/graphql/v1` | PostgREST | Yes | `Authorization` |
|
||||
| `/realtime/v1/api/*` | Realtime (REST) | Yes | `Authorization` |
|
||||
| `/realtime/v1/*` | Realtime (WebSocket) | Yes | `x-api-key` |
|
||||
| `/storage/v1/*` | Storage | No | `Authorization` |
|
||||
| `/functions/v1/*` | Edge Functions | No | - |
|
||||
| Route | Service | API key required | Header substitution |
|
||||
| -------------------- | -------------------- | ---------------- | ------------------- |
|
||||
| `/auth/v1/*` | Auth | Yes | `Authorization` |
|
||||
| `/rest/v1/*` | PostgREST | Yes | `Authorization` |
|
||||
| `/graphql/v1` | PostgREST | Yes | `Authorization` |
|
||||
| `/realtime/v1/api/*` | Realtime (REST) | Yes | `Authorization` |
|
||||
| `/realtime/v1/*` | Realtime (WebSocket) | Yes | `x-api-key` |
|
||||
| `/storage/v1/*` | Storage | No | `Authorization` |
|
||||
| `/functions/v1/*` | Edge Functions | No | - |
|
||||
|
||||
### Request flows
|
||||
|
||||
|
||||
@@ -31,21 +31,7 @@ If you already run [HAProxy](https://www.haproxy.com/), [Traefik](https://traefi
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Step 1: Remove public port bindings for API gateway
|
||||
|
||||
Comment out Kong's host port mappings in `docker-compose.yml` so that it's not exposed to the Internet:
|
||||
|
||||
```yaml
|
||||
kong:
|
||||
# ...
|
||||
ports:
|
||||
# - ${KONG_HTTP_PORT}:8000/tcp
|
||||
# - ${KONG_HTTPS_PORT}:8443/tcp
|
||||
```
|
||||
|
||||
Kong remains accessible to other containers on the internal Docker network.
|
||||
|
||||
### Step 2: Update environment variables
|
||||
### Step 1: Update environment variables
|
||||
|
||||
Update the URL configuration in your `.env` file to use your HTTPS domain:
|
||||
|
||||
@@ -62,7 +48,7 @@ PROXY_DOMAIN=your-domain.example.com
|
||||
CERTBOT_EMAIL=admin@your-domain.example.com
|
||||
```
|
||||
|
||||
### Step 3: Start the reverse proxy
|
||||
### Step 2: Start the reverse proxy
|
||||
|
||||
Pick one of the options below and use the corresponding Docker Compose overlay.
|
||||
|
||||
@@ -102,7 +88,7 @@ HTTP-to-HTTPS redirects are handled automatically by the `jonasal/nginx-certbot`
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Step 4: Verify HTTPS connection
|
||||
### Step 3: Verify HTTPS connection
|
||||
|
||||
```sh
|
||||
curl -I https://<your-domain>/auth/v1/
|
||||
@@ -137,13 +123,13 @@ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
|
||||
### Step 2: Configure Kong for SSL
|
||||
|
||||
Comment out Kong's HTTP port mapping in `docker-compose.yml`:
|
||||
Comment out Kong's **HTTP** port mapping in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
kong:
|
||||
# ...
|
||||
ports:
|
||||
# - ${KONG_HTTP_PORT}:8000/tcp
|
||||
#- ${KONG_HTTP_PORT}:8000/tcp
|
||||
```
|
||||
|
||||
Uncomment the certificate volume mounts and SSL environment variables in `docker-compose.yml`:
|
||||
|
||||
@@ -55,7 +55,7 @@ The most commonly used endpoints are implemented, and more will be added. Implem
|
||||
| ✅ [PutObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) | System Metadata:<br/> ✅ Content-Type<br/> ✅ Cache-Control<br/> ✅ Content-Disposition<br/> ✅ Content-Encoding<br/> ✅ Content-Language<br/> ✅ Expires<br/> ❌ Content-MD5<br/>❌ Object Lifecycle<br/>❌ Website:<br/> ❌ x-amz-website-redirect-location<br/>❌ SSE-C:<br/> ❌ x-amz-server-side-encryption<br/> ❌ x-amz-server-side-encryption-customer-algorithm<br/> ❌ x-amz-server-side-encryption-customer-key<br/> ❌ x-amz-server-side-encryption-customer-key-MD5<br/> ❌ x-amz-server-side-encryption-aws-kms-key-id<br/> ❌ x-amz-server-side-encryption-context<br/> ❌ x-amz-server-side-encryption-bucket-key-enabled<br/>❌ Request Payer:<br/> ❌ x-amz-request-payer<br/>❌ Tagging:<br/> ❌ x-amz-tagging<br/>❌ Object Locking:<br/> ❌ x-amz-object-lock-mode<br/> ❌ x-amz-object-lock-retain-until-date<br/> ❌ x-amz-object-lock-legal-hold<br/>❌ ACL:<br/> ❌ x-amz-acl<br/> ❌ x-amz-grant-full-control<br/> ❌ x-amz-grant-read<br/> ❌ x-amz-grant-read-acp<br/> ❌ x-amz-grant-write-acp<br/>❌ Bucket Owner:<br/> ❌ x-amz-expected-bucket-owner |
|
||||
| ✅ [DeleteObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html) | ❌ Multi-factor authentication:<br/> ❌ x-amz-mfa<br/>❌ Object Locking:<br/> ❌ x-amz-bypass-governance-retention<br/>❌ Request Payer:<br/> ❌ x-amz-request-payer<br/>❌ Bucket Owner:<br/> ❌ x-amz-expected-bucket-owner |
|
||||
| ✅ [DeleteObjects](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) | ❌ Multi-factor authentication:<br/> ❌ x-amz-mfa<br/>❌ Object Locking:<br/> ❌ x-amz-bypass-governance-retention<br/>❌ Request Payer:<br/> ❌ x-amz-request-payer<br/>❌ Bucket Owner:<br/> ❌ x-amz-expected-bucket-owner |
|
||||
| ✅ [ListMultipartUploads](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html) | ✅ Query Parameters:<br/> ✅ delimiter<br/> ✅ encoding-type<br/> ✅ key-marker<br/> ✅️ max-uploads<br/> ✅ prefix<br/> ✅ upload-id-marker |
|
||||
| ✅ [ListMultipartUploads](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html) | ✅ Query Parameters:<br/> ✅ delimiter<br/> ✅ encoding-type<br/> ✅ key-marker<br/> ✅️ max-uploads<br/> ✅ prefix<br/> ✅ upload-id-marker |
|
||||
| ✅ [CreateMultipartUpload](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html) | ✅ System Metadata:<br/> ✅ Content-Type<br/> ✅ Cache-Control<br/> ✅ Content-Disposition<br/> ✅ Content-Encoding<br/> ✅ Content-Language<br/> ✅ Expires<br/> ❌ Content-MD5<br/>❌ Website:<br/> ❌ x-amz-website-redirect-location<br/>❌ SSE-C:<br/> ❌ x-amz-server-side-encryption<br/> ❌ x-amz-server-side-encryption-customer-algorithm<br/> ❌ x-amz-server-side-encryption-customer-key<br/> ❌ x-amz-server-side-encryption-customer-key-MD5<br/> ❌ x-amz-server-side-encryption-aws-kms-key-id<br/> ❌ x-amz-server-side-encryption-context<br/> ❌ x-amz-server-side-encryption-bucket-key-enabled<br/>❌ Request Payer:<br/> ❌ x-amz-request-payer<br/>❌ Tagging:<br/> ❌ x-amz-tagging<br/>❌ Object Locking:<br/> ❌ x-amz-object-lock-mode<br/> ❌ x-amz-object-lock-retain-until-date<br/> ❌ x-amz-object-lock-legal-hold<br/>❌ ACL:<br/> ❌ x-amz-acl<br/> ❌ x-amz-grant-full-control<br/> ❌ x-amz-grant-read<br/> ❌ x-amz-grant-read-acp<br/> ❌ x-amz-grant-write-acp<br/>❌ Storage class:<br/> ❌ x-amz-storage-class<br/>❌ Bucket Owner:<br/> ❌ x-amz-expected-bucket-owner |
|
||||
| ✅ [CompleteMultipartUpload](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html) | ❌ Bucket Owner:<br/> ❌ x-amz-expected-bucket-owner<br/>❌ Request Payer:<br/> ❌ x-amz-request-payer |
|
||||
| ✅ [AbortMultipartUpload](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html) | ❌ Request Payer:<br/> ❌ x-amz-request-payer |
|
||||
|
||||
@@ -1,93 +1,139 @@
|
||||
---
|
||||
title = "Edge Function 401 error response"
|
||||
topics = [ "functions" ]
|
||||
keywords = [ "401", "error" ]
|
||||
database_id = "12e09c3f-a48b-4aea-9df3-90a25ef0021e"
|
||||
keywords = ["401", "error", "JWT", "authorization"]
|
||||
|
||||
[[errors]]
|
||||
http_status_code = 401
|
||||
message = "Invalid JWT"
|
||||
---
|
||||
|
||||
The function rejected the request for lacking the appropriate authorization headers.
|
||||
A 401 response from an Edge Function means either:
|
||||
|
||||
## Context for the error
|
||||
- The function failed the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
|
||||
- Your function's logic deliberately returned a 401 response
|
||||
|
||||
#### The JWT verification check
|
||||
## Quick triage
|
||||
|
||||
By default, edge functions are configured to check requests for a valid [legacy key](/docs/guides/api/api-keys#overview).
|
||||
Check the response body returned by the request
|
||||
|
||||
#### How the check causes 401 errors
|
||||
|
||||
The check fails and return a 401 permission error if:
|
||||
|
||||
- The request includes an invalid legacy key, or
|
||||
- The request uses the newer asymmetric keys instead of a legacy key.
|
||||
|
||||
#### What this check actually does
|
||||
|
||||
This validation provides limited security value. It only confirms that the request includes a legacy token associated with your project, such as the anon key.
|
||||
|
||||
It does not validate the user's identity or permissions beyond that.
|
||||
|
||||
Because the security check is weak, it was deprecated when used with the new asymmetric keys.
|
||||
|
||||
## Solving the error
|
||||
|
||||
### Step 1: Identifying the error
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
If the tests return a 401 but don't match the criteria below, the error is coming from your app logic, not the JWT check.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Inspecting the return message
|
||||
|
||||
When an edge function fails due to a platform 401 error, it returns the error:
|
||||
### Case 1: `"Invalid JWT"` or `"Missing authorization header"`
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "Invalid JWT"
|
||||
}
|
||||
{ "code": 401, "message": "Invalid JWT" }
|
||||
```
|
||||
|
||||
### Inspecting the logs
|
||||
```json
|
||||
{ "code": 401, "message": "Missing authorization header" }
|
||||
```
|
||||
|
||||
Your code may return a 401 error due to its own logic
|
||||
Both of these messages come from the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks)
|
||||
|
||||
Go to: [Built-in JWT check failures](#built-in-jwt-check-failures)
|
||||
|
||||
### Case 2: Custom message or empty body
|
||||
|
||||
If the response body contains a message you coded, or nothing at all, then your function code _did_ execute and returned a 401 itself.
|
||||
|
||||
Go to: [Your function returned a 401](#your-function-returned-a-401)
|
||||
|
||||
### Case 3: Not sure
|
||||
|
||||
Run this query in [Log Explorer](/dashboard/project/_/logs/explorer?q=SELECT%0A++++cast%28timestamp+AS+datetime%29++AS+timestamp%2C%0A++++req.pathname+++++++++++++++++AS+function_name%2C%0A%0A++++CASE%0A++++++++WHEN+metadata.execution_id+IS+NOT+NULL%0A++++++++++++THEN+%27your_code_returned_401%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+%28new_auth.prefix+IS+NOT+NULL+OR+legacy_payload.algorithm+<>+%27HS256%27%29%0A++++++++++++THEN+%27incompatible_keys%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A++++++++AND+%0A++++++++++++%28%0A++++++++++++++++%28legacy_auth_data.invalid+IS+NOT+NULL+OR+new_auth.error+IS+NOT+NULL%29%0A++++++++++++++++++++OR%0A++++++++++++++++legacy_payload.algorithm+%3D+%27HS256%27%0A++++++++++++%29%0A++++++++++++THEN+%27invalid_key%27%0A++++++++WHEN+metadata.execution_id+IS+NULL%0A+++++++++AND+legacy_auth_data+++++++IS+NULL%0A+++++++++AND+new_auth.prefix+IS+NULL%0A++++++++++++THEN+%27missing_auth_header%27%0A++++END+AS+cause%0A%0AFROM+function_edge_logs%0A%0A++++--+unnesting+metadata%0A++++CROSS+JOIN+UNNEST%28metadata%29++++++++++AS+metadata%0A++++CROSS+JOIN+UNNEST%28metadata.request%29++AS+req%0A++++CROSS+JOIN+UNNEST%28metadata.response%29+AS+res%0A++++--+unnesting+auth+details%0A++++LEFT+JOIN+UNNEST%28req.sb%29++++++++++++++++++++AS+sb%0A++++LEFT+JOIN+UNNEST%28sb.apikey%29+++++++++++++++++AS+apikey%0A++++LEFT+JOIN+UNNEST%28apikey.authorization%29++++++AS+new_auth%0A++++LEFT+JOIN+UNNEST%28sb.jwt%29++++++++++++++++++++AS+legacy_jwt%0A++++LEFT+JOIN+UNNEST%28legacy_jwt.authorization%29++AS+legacy_auth_data%0A++++LEFT+JOIN+UNNEST%28legacy_auth_data.payload%29++AS+legacy_payload%0A%0AWHERE+res.status_code+%3D+401%0AORDER+BY+timestamp+DESC%0ALIMIT+200) to classify recent 401s:
|
||||
|
||||
```sql
|
||||
select
|
||||
cast(timestamp as datetime) as timestamp,
|
||||
req.pathname as function_name,
|
||||
case
|
||||
when metadata.execution_id is not null then 'your_code_returned_401'
|
||||
when metadata.execution_id is null
|
||||
and (
|
||||
new_auth.prefix is not null
|
||||
or legacy_payload.algorithm != 'HS256'
|
||||
) then 'incompatible_keys'
|
||||
when metadata.execution_id is null
|
||||
and (
|
||||
(legacy_auth_data.invalid is not null or new_auth.error is not null)
|
||||
or legacy_payload.algorithm = 'HS256'
|
||||
) then 'invalid_key'
|
||||
when metadata.execution_id is null
|
||||
and legacy_auth_data is null
|
||||
and new_auth.prefix is null then 'missing_auth_header'
|
||||
end as cause
|
||||
from
|
||||
function_edge_logs
|
||||
-- unnesting metadata
|
||||
cross join UNNEST(metadata) as metadata
|
||||
cross join UNNEST(metadata.request) as req
|
||||
cross join UNNEST(metadata.response) as res
|
||||
-- unnesting auth details
|
||||
left join UNNEST(req.sb) as sb
|
||||
left join UNNEST(sb.apikey) as apikey
|
||||
left join UNNEST(apikey.authorization) as new_auth
|
||||
left join UNNEST(sb.jwt) as legacy_jwt
|
||||
left join UNNEST(legacy_jwt.authorization) as legacy_auth_data
|
||||
left join UNNEST(legacy_auth_data.payload) as legacy_payload
|
||||
where res.status_code = 401
|
||||
order by timestamp desc
|
||||
limit 50;
|
||||
```
|
||||
|
||||
Depending on the output, you can use this table to find the appropriate debugging section:
|
||||
|
||||
| Value | Go to |
|
||||
| ------------------------ | ------------------------------------------------------------- |
|
||||
| `your_code_returned_401` | [Your function returned a 401](#your-function-returned-a-401) |
|
||||
| `incompatible_keys` | [Incompatible key format](#incompatible-key-format) |
|
||||
| `invalid_key` | [Invalid key](#invalid-key) |
|
||||
| `missing_auth_header` | [Missing Authorization header](#missing-authorization-header) |
|
||||
|
||||
---
|
||||
|
||||
## Your function returned a 401
|
||||
|
||||
Your function ran, and somewhere in your code, its logic returned a 401.
|
||||
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
status: 401, // app logic returning a 401
|
||||
status: 401, // <-- you set this
|
||||
})
|
||||
```
|
||||
|
||||
A 401 status code alone doesn't confirm a JWT check failure by itself. The log must also lack an `execution_id`.
|
||||
**How to fix:**
|
||||
|
||||

|
||||
1. Search your function code for `401`. Look for explicit status codes on `Response` objects.
|
||||
2. Trace the condition that triggered it. If you're interacting with a third-party API in your code, that service may be returning 401 that you're forwarding in the response object.
|
||||
3. Add logging before the return so future occurrences leave a trace:
|
||||
|
||||
Instead of manually reviewing the logs, you can run the below query in the [log explorer](</dashboard/project/_/logs/explorer?q=SELECT+DISTINCT%0A++++req.pathname+AS+function_name,%0A++++res.status_code%0A++FROM+function_edge_logs%0A++CROSS+JOIN+UNNEST(metadata)+AS+metadata+%0A++CROSS+JOIN+UNNEST(metadata.request)+AS+req+%0A++CROSS+JOIN+UNNEST(metadata.response)+AS+res+%0AWHERE+status_code+=+401+AND+metadata.execution_id+IS+NULL%0ALIMIT+10;&its=&ite=%3E>) to get a list of functions that were impacted by the check:
|
||||
|
||||
```sql
|
||||
select distinct
|
||||
req.pathname as function_name,
|
||||
res.status_code
|
||||
from
|
||||
function_edge_logs
|
||||
cross join UNNEST(metadata) as metadata
|
||||
cross join UNNEST(metadata.request) as req
|
||||
cross join UNNEST(metadata.response) as res
|
||||
where status_code = 401 and metadata.execution_id is null
|
||||
limit 10;
|
||||
```js
|
||||
console.error('Returning 401 - reason:', reason)
|
||||
```
|
||||
|
||||
### Step 2: Disabling the JWT check
|
||||
See: [Error handling in Edge Functions](/docs/guides/functions/error-handling)
|
||||
|
||||
The JWT check provides minimal security benefits, so we now recommend handling authentication through app logic instead. See the [Edge Function Auth Doc](/docs/guides/functions/auth) for details.
|
||||
---
|
||||
|
||||
If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/_/settings/api-keys) or no longer need the JWT check, you can disable it using one of the three options below:
|
||||
## Built-in JWT check failures
|
||||
|
||||
Supabase Edge Functions have a legacy auth verification check that runs before your code. When it fails, your function never executes, and you get a 401 with `"Invalid JWT"` or `"Missing authorization header"` directly from the platform.
|
||||
|
||||
<Admonition type="deprecation">
|
||||
|
||||
Supabase now recommends turning off this built-in check and managing authentication directly in your function code, giving you more control over access. See [Securing Edge Functions](/docs/guides/functions/auth).
|
||||
|
||||
</Admonition>
|
||||
|
||||
The subsections below cover specific failure modes.
|
||||
|
||||
### Incompatible key format
|
||||
|
||||
Your project uses the [new asymmetric keys](/blog/jwt-signing-keys) for authentication. However, the [legacy auth verification check](/docs/guides/functions/development-tips#skipping-authorization-checks) only understands the legacy format.
|
||||
|
||||
**Fix:** Disable the built-in JWT check using one of the below methods and optionally [handle auth in your function code](/docs/guides/functions/auth)
|
||||
|
||||
<Accordion
|
||||
type="default"
|
||||
@@ -99,51 +145,49 @@ If you've migrated to [asymmetric keys (publishable/secret)](/dashboard/project/
|
||||
>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="In the Dashboard"
|
||||
header="Method A: Dashboard"
|
||||
id="item-1"
|
||||
>
|
||||
In the [Function Dashboard](/dashboard/project/_/functions/), under the offending function's detail tab, you can toggle off the check:
|
||||
|
||||

|
||||
In the [Functions Dashboard](/dashboard/project/_/functions/), open the affected function's `detail tab` and toggle off JWT verification.
|
||||
|
||||

|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="Method B: Supabase CLI"
|
||||
id="item-2"
|
||||
>
|
||||
|
||||
Redeploy the edge function from the [Supabase CLI](/docs/guides/functions/quickstart) with the `--no-verify-jwt` flag
|
||||
|
||||
```sh
|
||||
supabase functions deploy YOUR_FUNCTION_NAME --no-verify-jwt
|
||||
```
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="With the Management API"
|
||||
id="item-2"
|
||||
>
|
||||
|
||||
1. Generate a secret token in the [Account Preferences Dashboard](/dashboard/account/tokens)
|
||||
2. Get your project ID from the [General Settings](/dashboard/project/_/settings/general)
|
||||
3. Substitute in the `PROJECT_ID`, `FUNCTION_NAME`, and `SECRET_TOKEN` in below [management endpoint](/docs/reference/api/v1-update-a-function). Then execute the request from a terminal environment
|
||||
|
||||
```sh
|
||||
# remember to substitute in your PROJECT_ID, FUNCTION_NAME, and SECRET_TOKEN
|
||||
|
||||
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME?verify_jwt=false' \
|
||||
--request PATCH \
|
||||
--header 'Content-Type: application/vnd.denoland.eszip' \
|
||||
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
|
||||
--data '""'
|
||||
|
||||
````
|
||||
</AccordionItem>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="border-b mt-3 pb-3">
|
||||
<AccordionItem
|
||||
header="With the Supabase CLI"
|
||||
header="Method C: Management API"
|
||||
id="item-3"
|
||||
>
|
||||
Disable the legacy auth check with the [Supabase Management API](/docs/reference/api/introduction):
|
||||
|
||||
With the [Supabase CLI](/docs/guides/local-development/cli/getting-started), you can redeploy your edge function without the JWT check:
|
||||
1. Generate a token at [Account Preferences](/dashboard/account/tokens).
|
||||
2. Get your project ID from [General Settings](/dashboard/project/_/settings/general).
|
||||
3. Run:
|
||||
|
||||
```sh
|
||||
supabase functions deploy hello-world --no-verify-jwt
|
||||
curl 'https://api.supabase.com/v1/projects/PROJECT_ID/functions/FUNCTION_NAME' \
|
||||
--request PATCH \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
|
||||
--data '{"verify_jwt": false}'
|
||||
```
|
||||
|
||||
</AccordionItem>
|
||||
@@ -151,12 +195,43 @@ supabase functions deploy hello-world --no-verify-jwt
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
If you would like to continue using the JWT check, make sure that your [Supabase Client](/docs/guides/api/rest/client-libs) only uses your [legacy keys](/dashboard/project/_/settings/api-keys/legacy).
|
||||
### Invalid key
|
||||
|
||||
The built-in check is enabled and the key you sent doesn't match your project's keys.
|
||||
|
||||
**Fix (recommended):** Disable the built-in check using the steps in [Incompatible key format](#incompatible-key-format).
|
||||
|
||||
**Fix (alternative):** If you want to keep the built-in check, ensure you're sending a valid key. Use one of your [legacy API keys](/dashboard/project/_/settings/api-keys/legacy) with the [Supabase client library](/docs/guides/api/rest/client-libs) when making your request.
|
||||
|
||||
```js
|
||||
const supabase = createClient('https://xyzcompany.supabase.co', 'anon-key-or-service_role-key')
|
||||
```
|
||||
|
||||
### Missing authorization header
|
||||
|
||||
The built-in check is enabled but your request has no `Authorization` header at all.
|
||||
|
||||
If you're using a [Supabase client library](/docs/guides/api/rest/client-libs), the header is added automatically. If you're calling the function from an external client (cURL, fetch, etc.), you need to supply it:
|
||||
|
||||
```sh
|
||||
curl -L -X POST 'https://PROJECT_REF.supabase.co/functions/v1/hello-world' \
|
||||
-H 'Authorization: Bearer YOUR_ANON_OR_SERVICE_ROLE_KEY' \
|
||||
--data '{"name":"Functions"}'
|
||||
```
|
||||
|
||||
Alternatively, you can disable the built-in check entirely (see [Incompatible key format](#incompatible-key-format)).
|
||||
|
||||
---
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Securing Edge Functions](/docs/guides/functions/auth)
|
||||
- [Debugging Edge Functions](/docs/guides/functions/logging)
|
||||
- [Quickstart Deployment: Dashboard](/docs/guides/functions/quickstart-dashboard)
|
||||
- [Quickstart Deployment: CLI](/docs/guides/functions/quickstart)
|
||||
````
|
||||
- [Securing Edge Functions with Auth](/docs/guides/functions/auth)
|
||||
- [Logging Edge Function Requests](/docs/guides/functions/logging)
|
||||
- [Error Handling Edge Functions](/docs/guides/functions/error-handling)
|
||||
- [Quickstart: Dashboard deployment](/docs/guides/functions/quickstart-dashboard)
|
||||
- [Quickstart: CLI deployment](/docs/guides/functions/quickstart)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
- Check the [Discord](https://discord.com/channels/839993398554656828/1006358244786196510), [Supabase GitHub Discussions](https://github.com/orgs/supabase/discussions), and [Reddit page](https://www.reddit.com/r/Supabase/) for similar reports that can help with debugging
|
||||
- Open a [support ticket](/dashboard/support/new) for your project if the problem persists and you believe it is a platform issue
|
||||
@@ -8,7 +8,7 @@ database_id = "aaf9e673-64ae-460a-88e0-b83ea4963382"
|
||||
cli = [ "supabase-functions-deploy" ]
|
||||
---
|
||||
|
||||
Edge Functions have a 10MB source code limit. If your function exceeds this limit, deployment will fail.
|
||||
Edge Functions have a 20MB source code limit. If your function exceeds this limit, deployment will fail.
|
||||
|
||||
## Check your bundle size
|
||||
|
||||
|
||||
-1
@@ -43,7 +43,6 @@ This situation often arises in large, high-write tables (e.g., `your_table`, whi
|
||||
Since the wraparound prevention autovacuum cannot be stopped, the best approach is to provide the database with sufficient resources to complete the operation as quickly and efficiently as possible.
|
||||
|
||||
1. **Upgrade your Database Compute Instance:**
|
||||
|
||||
- **Action:** Temporarily scale up your instance's CPU (e.g., from `m6g.4xlarge` to `m6g.8xlarge` or higher).
|
||||
- **Why it helps:** More CPU cores and processing power will help the autovacuum operation run faster, reducing the overall time it impacts your database.
|
||||
- **Considerations:** This usually causes a brief downtime (typically 1-2 minutes) as the instance restarts. However, the autovacuum process is designed to pause and resume automatically.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
title = "How can I revoke execution of a Postgres function?"
|
||||
github_url = "https://github.com/orgs/supabase/discussions/17606"
|
||||
date_created = "2023-09-21T03:04:41+00:00"
|
||||
topics = [ "database", "functions" ]
|
||||
topics = [ "database" ]
|
||||
keywords = [ "functions", "permissions" ]
|
||||
database_id = "b7edb30b-beee-40ae-9b4f-666ef6411bb6"
|
||||
|
||||
|
||||
@@ -24,17 +24,14 @@ This method requires two steps - first delete in Supabase, then clean up in Verc
|
||||
2. **Clean up in Vercel**
|
||||
|
||||
After deleting from Supabase, clean up in Vercel:
|
||||
|
||||
- Navigate to your [Vercel dashboard](https://vercel.com/dashboard)
|
||||
- Go to the **Storage** tab
|
||||
- Click on the Supabase project (it will show as "uninstalled")
|
||||
|
||||

|
||||
|
||||
- Scroll down and find the **Settings** tab in the sidebar
|
||||
|
||||

|
||||
|
||||
- Click **Delete Database** to completely remove it
|
||||
|
||||

|
||||
@@ -84,7 +81,6 @@ If you want to move a Vercel-linked project to a different organization:
|
||||
2. **Clean up in Vercel**
|
||||
|
||||
After transferring, the project will show as "uninstalled" in Vercel but still appear under Storage. You need to manually delete it:
|
||||
|
||||
- Navigate to your [Vercel dashboard](https://vercel.com/dashboard)
|
||||
- Go to the **Storage** tab
|
||||
- Click on the Supabase project (it will show as "uninstalled")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title = "'Manually created databases are not visible in the Supabase Dashboard'"
|
||||
topics = [ "auth", "cli", "database", "functions", "platform", "storage" ]
|
||||
topics = [ "database"]
|
||||
keywords = []
|
||||
database_id = "f6420e72-ea67-4825-b3f7-e722ea5c0d96"
|
||||
---
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
title = "NEW variable is null in a trigger function."
|
||||
github_url = "https://github.com/orgs/supabase/discussions/15934"
|
||||
date_created = "2023-07-20T19:05:52+00:00"
|
||||
topics = [ "database", "functions" ]
|
||||
topics = [ "database" ]
|
||||
keywords = [ "trigger", "function", "statement", "row", "NEW", "OLD", "SQL" ]
|
||||
database_id = "f5cabb97-aae3-4fb5-b597-4e7857821753"
|
||||
---
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ The email templating system often uses a language like Go, which has built-in se
|
||||
You may observe two distinct issues:
|
||||
|
||||
1. **The `#ZgotmplZ` Error**:
|
||||
|
||||
- **Cause**: This error occurs when you attempt to use a non-standard URL scheme (like `your-app-scheme://` for a mobile app deep link) directly within a template variable like `{{ .SiteURL }}` in your email templates. Go's security features sanitize this perceived "unsafe" URL, replacing it with the `#ZgotmplZ` placeholder. This prevents the link from being rendered correctly and thus from working.
|
||||
|
||||
2. **The 'Cannot Parse Response' Error**:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title = "PostgREST not recognizing new columns, tables, views or functions"
|
||||
topics = [ "cli", "platform", "database", "functions" ]
|
||||
topics = [ "platform", "database" ]
|
||||
keywords = []
|
||||
database_id = "c3481097-1a32-4e94-b4f6-7aeb88132a41"
|
||||
---
|
||||
|
||||
@@ -37,7 +37,6 @@ In most cases, quota overages are accidental rather than intentional:
|
||||
## What to do if your project is suspended
|
||||
|
||||
1. **Open a support ticket**: [Contact support](/dashboard/support/new) and include:
|
||||
|
||||
- Your project reference ID
|
||||
- A description of your Realtime use case (what features use Broadcast, Presence, or Postgres Changes)
|
||||
- An estimate of your expected concurrent connections and message throughput
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title = "Running EXPLAIN ANALYZE on functions"
|
||||
topics = [ "database", "functions" ]
|
||||
keywords = []
|
||||
topics = [ "database" ]
|
||||
keywords = ["postgres", "EXPLAIN", "database functions"]
|
||||
database_id = "1d62cace-c0f6-47a0-8690-002a797da33b"
|
||||
|
||||
[api]
|
||||
|
||||
-3
@@ -13,12 +13,10 @@ This typically indicates an authentication failure where the database connection
|
||||
**To resolve this, consider one of the following solutions:**
|
||||
|
||||
1. **Check Network Bans:**
|
||||
|
||||
- Navigate to your project's [Database Settings](/dashboard/project/_/database/settings) page.
|
||||
- Review any listed IP addresses that are blocked. Remove any entries that correspond to your current connection and then try the CLI action again.
|
||||
|
||||
2. **Use the old Password-Based authentication flow instead:**
|
||||
|
||||
- Provide your database password directly through an environment variable when running the CLI command.
|
||||
|
||||
```bash
|
||||
@@ -26,7 +24,6 @@ This typically indicates an authentication failure where the database connection
|
||||
```
|
||||
|
||||
3. **Skip the Pooler and connect directly to the database with the Supabase CLI (Requires IPv6):**
|
||||
|
||||
- If your network supports IPv6, you can use the beta CLI version with the `--skip-pooler` flag to bypass the connection pooler to avoid this particular issue.
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title = "'Supabase Storage: Inefficient folder operations and hierarchical RLS challenges'"
|
||||
topics = [ "functions", "storage" ]
|
||||
topics = [ "storage" ]
|
||||
keywords = []
|
||||
database_id = "3b52daf2-d78d-4630-8e9f-8bf5d90208bf"
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title = "Using SQLAlchemy with Supabase"
|
||||
github_url = "https://github.com/orgs/supabase/discussions/27071"
|
||||
date_created = "2024-06-06T16:27:43+00:00"
|
||||
topics = [ "database", "supavisor", "self-hosting", "functions" ]
|
||||
topics = [ "database", "supavisor", "self-hosting" ]
|
||||
keywords = [ "sqlalchemy", "ipv6", "pool", "database", "connection" ]
|
||||
database_id = "da4efcda-cd02-4001-965b-f0294300152f"
|
||||
---
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
title = "Why do I see Auth & API requests in the dashboard? My app has no users"
|
||||
github_url = "https://github.com/orgs/supabase/discussions/21579"
|
||||
date_created = "2024-02-26T21:39:41+00:00"
|
||||
topics = [ "auth", "platform", "database", "realtime", "functions" ]
|
||||
topics = [ "auth", "platform", "database", "realtime" ]
|
||||
keywords = [ "dashboard", "health", "endpoints" ]
|
||||
database_id = "f85935d8-e871-4463-8288-f118f4e24afd"
|
||||
---
|
||||
|
||||
@@ -265,7 +265,6 @@ Thanks [@wiverson](https://github.com/wiverson) for the work in this release!
|
||||
## 0.9.0 - 2023-04-12
|
||||
|
||||
- Update dependency: gotrue-csharp@3.1.0
|
||||
|
||||
- [Minor] Implements PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather
|
||||
than a `string`.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IconPanel } from 'ui-patterns/IconPanel'
|
||||
import SqlToRest from 'ui-patterns/SqlToRest'
|
||||
import { Heading } from 'ui/src/components/CustomHTMLElements'
|
||||
import { AiPromptsIndex } from '~/app/guides/getting-started/ai-prompts/[slug]/AiPromptsIndex'
|
||||
import { AiSkillsIndex } from '~/app/guides/getting-started/ai-skills/AiSkillsIndex'
|
||||
import { AppleSecretGenerator } from '~/components/AppleSecretGenerator'
|
||||
import AuthProviders from '~/components/AuthProviders'
|
||||
import { AuthSmsProviderConfig } from '~/components/AuthSmsProviderConfig'
|
||||
@@ -44,6 +45,7 @@ const components = {
|
||||
AccordionItem,
|
||||
Admonition: AdmonitionWithMargin,
|
||||
AiPromptsIndex,
|
||||
AiSkillsIndex,
|
||||
AuthSmsProviderConfig,
|
||||
AppleSecretGenerator,
|
||||
AuthProviders,
|
||||
|
||||
@@ -10,13 +10,13 @@ import commonClientLibSections from '~/spec/common-client-libs-sections.json' wi
|
||||
function hasIntro(sections: typeof commonClientLibSections, excludeName?: string) {
|
||||
return Boolean(
|
||||
sections[0]?.type === 'markdown' &&
|
||||
sections[0]?.slug === 'introduction' &&
|
||||
(!excludeName ||
|
||||
!(
|
||||
'excludes' in sections[0] &&
|
||||
Array.isArray(sections[0].excludes) &&
|
||||
sections[0].excludes?.includes(excludeName)
|
||||
))
|
||||
sections[0]?.slug === 'introduction' &&
|
||||
(!excludeName ||
|
||||
!(
|
||||
'excludes' in sections[0] &&
|
||||
Array.isArray(sections[0].excludes) &&
|
||||
sections[0].excludes?.includes(excludeName)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -265,15 +265,35 @@ function normalizeComment(original: TypedocComment | Comment | undefined): Comme
|
||||
if (t.name) {
|
||||
return [t.name, t.content.map((p) => p.text).join('')]
|
||||
}
|
||||
const first = t.content[0]
|
||||
// TypeDoc may produce a tag with an empty content array (e.g. a tag with
|
||||
// no body text). Guard here so the rest of the parsing can safely assume
|
||||
// a non-empty first element.
|
||||
if (!first) return ['', '']
|
||||
const newline = first.text.indexOf('\n')
|
||||
const name = (newline >= 0 ? first.text.slice(0, newline) : first.text).trim()
|
||||
const tail = newline >= 0 ? first.text.slice(newline + 1) : ''
|
||||
const body = [{ ...first, text: tail }, ...t.content.slice(1)].map((p) => p.text).join('')
|
||||
if (!t.content[0]) return ['', '']
|
||||
// The tag name may span multiple content nodes (e.g. "With " + "`select()`"
|
||||
// when the name contains inline code). Scan through nodes until a newline.
|
||||
const nameParts: string[] = []
|
||||
let bodyStartIdx = 0
|
||||
let bodyStartText = ''
|
||||
let foundNewline = false
|
||||
for (let i = 0; i < t.content.length; i++) {
|
||||
const part = t.content[i]
|
||||
const newline = part.text.indexOf('\n')
|
||||
if (newline >= 0) {
|
||||
nameParts.push(part.text.slice(0, newline))
|
||||
bodyStartIdx = i
|
||||
bodyStartText = part.text.slice(newline + 1)
|
||||
foundNewline = true
|
||||
break
|
||||
} else {
|
||||
nameParts.push(part.text)
|
||||
}
|
||||
}
|
||||
const name = nameParts.join('').trim()
|
||||
const body = foundNewline
|
||||
? [{ text: bodyStartText }, ...t.content.slice(bodyStartIdx + 1)]
|
||||
.map((p) => p.text)
|
||||
.join('')
|
||||
: ''
|
||||
return [name, body]
|
||||
}
|
||||
|
||||
@@ -539,9 +559,14 @@ function parseMethod(
|
||||
let { params, ret, comment } = parseSignature(signature, map)
|
||||
|
||||
// When a method has multiple overload signatures, TypeDoc places the shared
|
||||
// JSDoc on the method node rather than any individual signature.
|
||||
if (!comment && node.comment) {
|
||||
comment = normalizeComment(node.comment)
|
||||
// JSDoc on the method node rather than any individual signature. Always merge
|
||||
// node.comment as the base so that block tags (@remarks, @example, etc.) are
|
||||
// not lost when overload signatures already carry a minimal summary comment.
|
||||
if (node.comment) {
|
||||
const nodeComment = normalizeComment(node.comment)
|
||||
if (nodeComment) {
|
||||
comment = { ...nodeComment, ...comment }
|
||||
}
|
||||
}
|
||||
|
||||
const types: MethodTypes = {
|
||||
|
||||
@@ -200,7 +200,7 @@ function ParamOrTypeDetails({ paramOrType }: { paramOrType: object }) {
|
||||
'description' in paramOrType
|
||||
? (paramOrType.description as string)
|
||||
: isFromTypespec(paramOrType)
|
||||
? paramOrType.comment?.shortText ?? ''
|
||||
? (paramOrType.comment?.shortText ?? '')
|
||||
: ''
|
||||
|
||||
const subContent =
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -322,7 +322,6 @@ export function McpConfigPanel() {
|
||||
: 'Project selection is only available for the hosted platform.'}
|
||||
</p>
|
||||
<McpConfigPanelBase
|
||||
basePath="/docs"
|
||||
className="mt-6"
|
||||
projectRef={project?.ref}
|
||||
theme={resolvedTheme as 'light' | 'dark'}
|
||||
|
||||
Loaded 100 of 1228 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user