Merge branch 'master' into feat/embedded

This commit is contained in:
Alaister Young committed 2025-02-04 13:54:28 +08:00
commit 5a24c00324
1042 files changed
+60527 -27686

No files matched your search

+1 -2
View File
@@ -1,5 +1,6 @@
/packages/shared-data/pricing.ts @roryw10 @kevcodez
/packages/shared-data/plans.ts @roryw10 @kevcodez
/packages/common/telemetry-constants.ts @4L3k51 @loong @pamelachia
/apps/studio/ @supabase/Dashboard
@@ -17,5 +18,3 @@
/apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
/apps/studio/lib/constants/telemetry.ts @4L3k51 @loong @pamelachia
@@ -29,17 +29,22 @@ jobs:
sparse-checkout: |
apps/docs
- name: Set up Node.js
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm i
- name: Run Troubleshooting script
run: npm --prefix=apps/docs run troubleshooting:sync
run: pnpm run -F docs troubleshooting:sync
- name: Create Pull Request
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
@@ -1,14 +1,16 @@
name: Playwright Tests
name: Local Studio Tests
on:
push:
branches: [master]
paths:
- 'apps/studio/**'
- 'tests/local-studio-tests/**'
- 'pnpm-lock.yaml'
pull_request:
branches: [master]
paths:
- 'apps/studio/**'
- 'tests/local-studio-tests/**'
- 'pnpm-lock.yaml'
# Cancel old builds on new commit for same workflow + branch/PR
@@ -37,14 +39,14 @@ jobs:
- name: Install dependencies
run: pnpm i
- name: Install Playwright Browsers
run: pnpm -C playwright-tests exec playwright install --with-deps
run: pnpm -C tests/local-studio-tests exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm run test:playwright
run: pnpm test:local-studio
# mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable.
# continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-tests/playwright-report/
path: tests/local-studio-tests/playwright-report/
retention-days: 7
+2 -3
View File
@@ -9,13 +9,12 @@ apps/**/out
apps/www/schema.sql
apps/www/public/images/*
apps/docs/**/generated/*
apps/docs/examples
examples/slack-clone/nextjs-slack-clone/full-schema.sql
# ignore files with custom js formatting
apps/docs/pages/guides/auth/*.mdx
apps/docs/pages/guides/integrations/*.mdx
apps/studio/public
apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/design-system/__registry__
packages/icons/__registry__
packages/icons/src/icons/*.ts
packages/icons/src/icons/*.ts
+2 -2
View File
@@ -52,10 +52,10 @@ You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and
- [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API
- [PostgREST](http://postgrest.org/) is a web server that turns your PostgreSQL database directly into a RESTful API.
- [GoTrue](https://github.com/supabase/gotrue) is a JWT based API for managing users and issuing JWT tokens.
- [Storage](https://github.com/supabase/storage-api) provides a RESTful interface for managing Files stored in S3, using Postgres to manage permissions.
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API
- [pg_graphql](http://github.com/supabase/pg_graphql/) a PostgreSQL extension that exposes a GraphQL API.
- [postgres-meta](https://github.com/supabase/postgres-meta) is a RESTful API for managing your Postgres, allowing you to fetch tables, add roles, and run queries, etc.
- [Kong](https://github.com/Kong/kong) is a cloud-native API gateway.
+11
View File
@@ -2161,6 +2161,17 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
chunks: []
},
"logs-bar-chart": {
name: "logs-bar-chart",
type: "components:example",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/example/logs-bar-chart")),
source: "",
files: ["registry/default/example/logs-bar-chart.tsx"],
category: "undefined",
subcategory: "undefined",
chunks: []
},
"chart-area-axes": {
name: "chart-area-axes",
type: "components:block",
+2 -3
View File
@@ -1,17 +1,16 @@
'use client'
import * as React from 'react'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { TooltipProvider_Shadcn_ } from 'ui'
import { TooltipProvider } from 'ui'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<JotaiProvider>
<NextThemesProvider {...props}>
<TooltipProvider_Shadcn_ delayDuration={0}>{children}</TooltipProvider_Shadcn_>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</NextThemesProvider>
</JotaiProvider>
)
+5
View File
@@ -117,6 +117,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/multi-select',
items: [],
},
{
title: 'Logs Bar Chart',
href: '/docs/fragments/logs-bar-chart',
items: [],
},
],
},
{
@@ -0,0 +1,21 @@
---
title: Logs Bar Chart
description: A stacked bar chart that displays logs errors and successes.
fragment: true
---
<ComponentPreview name="logs-bar-chart" peekCode wide />
## Usage
This component is used to display the bar chart in logs pages.
It requires that the `data` prop is an array of objects with the following shape:
```ts
type LogsBarChartDatum = {
timestamp: string
ok_count: number
error_count: number
}
```
@@ -0,0 +1,21 @@
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
export default function LogsBarChartDemo() {
const data = Array.from({ length: 100 }, (_, i) => {
const date = new Date()
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
return {
timestamp: date.toISOString(),
ok_count: Math.floor(Math.random() * 100), // Random value 0-99
error_count: Math.floor(Math.random() * 50), // Random value 0-50
warning_count: Math.floor(Math.random() * 50), // Random value 0-50
}
}).reverse()
return (
<div className="w-full">
<LogsBarChart data={data} />
</div>
)
}
@@ -1,22 +1,16 @@
import { Button } from 'ui'
import {
Tooltip_Shadcn_,
TooltipContent_Shadcn_,
TooltipProvider_Shadcn_,
TooltipTrigger_Shadcn_,
} from 'ui'
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
export default function TooltipDemo() {
return (
<TooltipProvider_Shadcn_>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button type="outline">Hover</Button>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_>
</TooltipTrigger>
<TooltipContent>
<p>Add to library</p>
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</TooltipProvider_Shadcn_>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
+5
View File
@@ -1201,4 +1201,9 @@ export const examples: Registry = [
type: 'docs:example',
files: ['example/expanding-textarea-demo.tsx'],
},
{
name: 'logs-bar-chart',
type: 'components:example',
files: ['example/logs-bar-chart.tsx'],
},
]
+3
View File
@@ -26,3 +26,6 @@ yarn-error.log*
# Sitemap
public/sitemap.xml
# Copied examples folder
/examples/
@@ -1,21 +0,0 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug: string[] }
const GuidePage = async ({ params }: { params: Params }) => {
const data = await getGuidesMarkdown(params)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams()
const generateMetadata = genGuideMeta(getGuidesMarkdown)
export default GuidePage
export { generateStaticParams, generateMetadata }
@@ -1,61 +0,0 @@
import { cache, type PropsWithChildren } from 'react'
import { IS_PLATFORM } from 'common'
import { NavMenuSection } from '~/components/Navigation/Navigation.types'
import Layout from '~/layouts/guides'
import { supabaseMisc } from '~/lib/supabaseMisc'
import { getAiPrompts } from './getting-started/ai-prompts/[slug]/AiPrompts.utils'
// Revalidate occasionally to pick up changes to partners
// 60 seconds/minute * 60 minutes/hour * 24 hours/day
// export const revalidate = 86_400
const GuidesLayout = async ({ children }: PropsWithChildren) => {
const [partnerNavItems, promptNavItems] = await Promise.all([getPartners(), getPrompts()])
const additionalNavItems =
partnerNavItems.length > 0 || promptNavItems.length > 0
? { integrations: partnerNavItems, prompts: promptNavItems }
: undefined
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
async function getPrompts() {
const prompts = await getAiPrompts()
return prompts.map(
(prompt) =>
({
name: prompt.heading,
url: `/guides/getting-started/ai-prompts/${prompt.filename}`,
}) as Partial<NavMenuSection>
)
}
const getPartners = cache(getPartnersImpl)
async function getPartnersImpl() {
if (!IS_PLATFORM) return []
const { data, error } = await supabaseMisc()
.from('partners')
.select('slug, title')
.eq('approved', true)
.eq('type', 'technology')
.order('title')
if (error) {
console.error(new Error('Error fetching partners', { cause: error }))
}
const partnerNavItems = (data ?? []).map(
(partner) =>
({
name: partner.title,
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
}) as Partial<NavMenuSection>
)
return partnerNavItems
}
export default GuidesLayout
@@ -1,106 +0,0 @@
import ReactMarkdown from 'react-markdown'
import { CodeBlock } from 'ui'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import { type TOCHeader } from '~/components/GuidesTableOfContents'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import type { Parameter } from '~/lib/refGenerator/refTypes'
import specFile from '~/spec/cli_v1_config.yaml' with { type: 'yml' }
const meta = {
title: 'Supabase CLI config',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/cli/config',
meta,
}))
const tocList: TOCHeader[] = []
const content = specFile.info.tags.map((tag: { id: string; title: string }, id: number) => {
tocList.push({ id: `${id}`, text: tag.title, link: `${tag.id}-config`, level: 2 })
return (
<div>
<Heading tag="h2">{tag.title} Config</Heading>
{specFile.parameters
.filter((param: Parameter) => param.tags && param.tags[0] === tag.id)
.map((parameter: Parameter, id: number) => {
tocList.push({ id: `${id}`, text: parameter.id, link: `#${parameter.id}`, level: 3 })
return <Info parameter={parameter} />
})}
</div>
)
})
const Config = () => {
const editLink = newEditLink('supabase/supabase/blob/master/apps/docs/spec/cli_v1_config.yaml')
return (
<GuideTemplate meta={meta} editLink={editLink}>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>{content}</div>
</GuideTemplate>
)
}
function Info({ parameter }: { parameter: Parameter }) {
return (
<div className="mt-8">
<div>
<Heading tag="h3" parseAnchors={false} customAnchor={parameter.id}>
<code>{parameter.title}</code>
</Heading>
<div className="border-b pb-8" key={parameter.id}>
<div className=" mb-16">
<div>
<table className="table-auto">
<thead>
<tr>
<th className="text-left">Name</th>
<th className="text-left">Default</th>
<th className="text-left">Required</th>
</tr>
</thead>
<tbody>
<tr>
<td>{parameter.id}</td>
<td>{parameter.default ? parameter.default.toString() : 'None'}</td>
<td>{parameter.required !== undefined && parameter.required.toString()}</td>
</tr>
</tbody>
</table>
</div>
<div className="mb-4 scroll-mt-16 mt-0 ">
<p className="text-sm font-bold uppercase">Description</p>
<ReactMarkdown>{parameter.description}</ReactMarkdown>
</div>
{parameter.usage && (
<div className="mb-4 scroll-mt-16 mt-0 ">
<p className="text-sm font-bold uppercase">Usage</p>
<CodeBlock className="useless-code-block-class" language="py">
{parameter.usage}
</CodeBlock>
</div>
)}
{parameter.links && (
<div>
<p className="text-sm font-bold uppercase">See also</p>
<ul>
{parameter.links.map((link) => (
<li key={link.link}>
<a href={link.link}>{link.name}</a>
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
</div>
)
}
export default Config
export { generateMetadata }
@@ -1,146 +0,0 @@
import matter from 'gray-matter'
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { notFound } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { isValidGuideFrontmatter } from '~/lib/docs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import {
terraformDocsBranch,
terraformDocsDocsDir,
terraformDocsOrg,
terraformDocsRepo,
} from '../terraformConstants'
// Each external docs page is mapped to a local page
const pageMap = [
{
remoteFile: 'README.md',
meta: {
title: 'Terraform Provider',
},
useRoot: true,
},
{
slug: 'tutorial',
remoteFile: 'tutorial.md',
meta: {
title: 'Using the Supabase Terraform Provider',
},
},
]
interface Params {
slug?: string[]
}
const TerraformDocs = async ({ params }: { params: Params }) => {
const { meta, ...data } = await getContent(params)
const options = {
mdxOptions: {
remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
} as SerializeOptions
return <GuideTemplate mdxOptions={options} meta={meta} {...data} />
}
/**
* The GitHub repo uses relative links, which don't lead to the right locations
* in docs.
*
* @param url The original link, as written in the Markdown file
* @returns The rewritten link
*/
const urlTransform: UrlTransformFunction = (url: string) => {
try {
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const getBasename = (pathname: string) =>
pathname.endsWith('.md') ? pathname.replace(/\.md$/, '') : pathname
const stripLeadingPrefix = (pathname: string) => pathname.replace(/^\//, '')
const stripLeadingDocs = (pathname: string) => pathname.replace(/^docs\//, '')
const relativePath = stripLeadingPrefix(getBasename(pathname))
const page = pageMap.find(
({ remoteFile, useRoot }) =>
(useRoot && `${relativePath}.md` === remoteFile) ||
(!useRoot && `${stripLeadingDocs(relativePath)}.md` === remoteFile)
)
if (page) {
return 'terraform' + `/${page.slug}` + hash
}
// If we don't have this page in our docs, link to GitHub repo
return `https://github.com/${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}${pathname}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
/**
* Fetch markdown from external repo
*/
const getContent = async ({ slug }: Params) => {
const [requestedSlug] = slug ?? []
const page = pageMap.find((page) => page.slug === requestedSlug)
if (!page) {
notFound()
}
const { meta, remoteFile, useRoot } = page
const editLink = newEditLink(
`${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()
// Strip out HTML comments
rawContent = rawContent.replace(/<!--.*?-->/, '')
let { content, data } = matter(rawContent)
// Remove the title from the content so it isn't duplicated in the final display
content = removeRedundantH1(content)
Object.assign(meta, data)
if (!isValidGuideFrontmatter(meta)) {
throw Error('Guide frontmatter is invalid.')
}
return {
pathname:
`/guides/platform/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
meta,
content,
editLink,
}
}
const generateStaticParams = async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] }))
const generateMetadata = genGuideMeta(getContent)
export default TerraformDocs
export { generateStaticParams, generateMetadata }
@@ -1,410 +0,0 @@
import { codeBlock } from 'common-tags'
import { Check, PlusCircle } from 'lucide-react'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import {
CodeBlock,
Heading,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { TabPanel, Tabs } from '~/features/ui/Tabs'
import {
terraformDocsBranch,
terraformDocsDocsDir,
terraformDocsOrg,
terraformDocsRepo,
} from '../terraformConstants'
const meta = {
title: 'Terraform Provider reference',
subtitle: 'Resources and data sources available through the Terraform Provider',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/platform/terraform/reference',
meta,
}))
function ProviderSettings({ schema }: { schema: any }) {
const attributes = schema.block.attributes
const example = codeBlock`
provider "supabase" {
${Object.keys(attributes).map(
(attribute) =>
`${attribute} = ${attributes[attribute].type === 'string' ? `""` : '<value>'}`
)}
}
`
return (
<section aria-labelledby="provider-settings" className="prose max-w-none">
<Heading tag="h2">Provider settings</Heading>
<p>
Use these settings to configure your Supabase provider and authenticate to your Supabase
project.
</p>
<Heading tag="h3">Example usage</Heading>
<CodeBlock className="not-prose">{example}</CodeBlock>
<Heading tag="h3">Details</Heading>
{/* extra div because width restriction doesn't work on table itself */}
<div className="w-full overflow-auto">
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Type</th>
<th>Optional</th>
<th>Sensitive</th>
</tr>
</thead>
<tbody>
{Object.keys(schema.block.attributes).map((attribute) => (
<tr key={attribute}>
<td>{attribute}</td>
<td>
<ReactMarkdown>{attributes[attribute].description}</ReactMarkdown>
</td>
<td>{attributes[attribute].type}</td>
<td className="align-middle">
{attributes[attribute].optional && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{attributes[attribute].sensitive && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}
function Resources({ schema }: { schema: any }) {
return (
<section aria-labelledby="resources" className="prose max-w-none">
<Heading tag="h2">Resources</Heading>
<p>You can configure these resources using the Supabase Terraform provider:</p>
<Tabs>
{Object.keys(schema).map((resource) => (
<TabPanel id={resource} label={resource}>
<Heading tag="h4">Example usage</Heading>
<CodeBlock className="not-prose">{codeBlock`
resource "${resource}" "<label>" {
${Object.keys(schema[resource].block.attributes)
.filter((attribute) => !schema[resource].block.attributes[attribute].computed)
.map(
(attribute) =>
`${attribute} = ${
schema[resource].block.attributes[attribute].type === 'string'
? `""`
: '<value>'
}`
)}
}
`}</CodeBlock>
<Heading tag="h4">Details</Heading>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Type</th>
<th>Required</th>
<th>Optional</th>
<th>Read-only</th>
</tr>
</thead>
<tbody>
{Object.keys(schema[resource].block.attributes).map((attribute) => (
<tr key={attribute}>
<td>
<code>{attribute}</code>
</td>
<td>
<ReactMarkdown>
{schema[resource].block.attributes[attribute].description}
</ReactMarkdown>
</td>
<td>
{schema[resource].block.attributes[attribute].type ?? (
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center justify-between gap-2">
Nested type
<PlusCircle size={14} className="shrink-0" />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="max-h-[50vh] overflow-auto">
<ul>
{Object.keys(
schema[resource].block.attributes[attribute].nested_type.attributes
).map((nestedAttribute) => (
<li key={nestedAttribute}>
{nestedAttribute}
<ul>
<li>
<ReactMarkdown className="*:!m-0">
{
schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].description
}
</ReactMarkdown>
</li>
<li>
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].type ?? 'nested type'}
</li>
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].required && <li>Required</li>}
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].optional && <li>Optional</li>}
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].computed && <li>Read-only</li>}
</ul>
</li>
))}
</ul>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
</td>
<td className="align-middle">
{schema[resource].block.attributes[attribute].required && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[resource].block.attributes[attribute].optional && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[resource].block.attributes[attribute].computed && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</TabPanel>
))}
</Tabs>
</section>
)
}
function DataSources({ schema }: { schema: any }) {
return (
<section aria-labelledby="data-sources" className="prose max-w-none">
<Heading tag="h2">Data sources</Heading>
<p>You can read these resources using the Supabase Terraform provider:</p>
<Tabs>
{Object.keys(schema).map((dataSource) => (
<TabPanel id={dataSource} label={dataSource}>
<Heading tag="h4">Example usage</Heading>
<CodeBlock className="not-prose">{codeBlock`
resource "${dataSource}" "all" {
${Object.keys(schema[dataSource].block.attributes)
.filter(
(attribute) => !schema[dataSource].block.attributes[attribute].computed
)
.map(
(attribute) =>
`${attribute} = ${
schema[dataSource].block.attributes[attribute].type === 'string'
? `""`
: '<value>'
}`
)}
}
`}</CodeBlock>
<Heading tag="h4">Details</Heading>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Type</th>
<th>Required</th>
<th>Optional</th>
<th>Read-only</th>
</tr>
</thead>
<tbody>
{Object.keys(schema[dataSource].block.attributes).map((attribute) => (
<tr key={attribute}>
<td>
<code>{attribute}</code>
</td>
<td>
<ReactMarkdown>
{schema[dataSource].block.attributes[attribute].description}
</ReactMarkdown>
</td>
<td>
{schema[dataSource].block.attributes[attribute].type ?? (
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center justify-between gap-2">
Nested type
<PlusCircle size={14} />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="max-h-[50vh] overflow-auto">
{schema[dataSource].block.attributes[attribute].nested_type
.nesting_mode === 'set' && 'Array of:'}
<ul>
{Object.keys(
schema[dataSource].block.attributes[attribute].nested_type
.attributes
).map((nestedAttribute) => (
<li key={nestedAttribute}>
{nestedAttribute}
<ul>
<li>
<ReactMarkdown className="*:!m-0">
{
schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].description
}
</ReactMarkdown>
</li>
<li>
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].type ?? 'nested type'}
</li>
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].required && <li>Required</li>}
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].optional && <li>Optional</li>}
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].computed && <li>Read-only</li>}
</ul>
</li>
))}
</ul>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
</td>
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].required && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].optional && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].computed && (
<>
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</TabPanel>
))}
</Tabs>
</section>
)
}
const TerraformReferencePage = async () => {
const { schema } = await getSchema()
const editLink = newEditLink('supabase/terraform-provider-supabase')
return (
<GuideTemplate meta={meta} editLink={editLink}>
The Terraform Provider provices access to{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/resources"
rel="noopener noreferrer"
>
resources
</Link>{' '}
and{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/data-sources"
rel="noreferrer noopener"
>
data sources
</Link>
. Resources are infrastructure objects, such as a Supabase project, that you can declaratively
configure. Data sources are sources of information about your Supabase instances.
<ProviderSettings
schema={schema.provider_schemas['registry.terraform.io/supabase/supabase'].provider}
/>
<Resources
schema={schema.provider_schemas['registry.terraform.io/supabase/supabase'].resource_schemas}
/>
<DataSources
schema={
schema.provider_schemas['registry.terraform.io/supabase/supabase'].data_source_schemas
}
/>
</GuideTemplate>
)
}
/**
* Fetch JSON schema from external repo
*/
const getSchema = async () => {
let response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${terraformDocsDocsDir}/schema.json`
)
if (!response.ok) throw Error('Failed to fetch Terraform JSON schema from GitHub')
const schema = await response.json()
return {
schema,
}
}
export default TerraformReferencePage
export { generateMetadata }
@@ -1,9 +0,0 @@
/**
* Information on where to fetch docs content
*/
const terraformDocsOrg = 'supabase'
const terraformDocsRepo = 'terraform-provider-supabase'
const terraformDocsBranch = 'v1.1.3'
const terraformDocsDocsDir = 'docs'
export { terraformDocsOrg, terraformDocsRepo, terraformDocsBranch, terraformDocsDocsDir }
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const AiGuidePage = async ({ params }: { params: Params }) => {
const slug = ['ai', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('ai')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['ai', ...(params.slug ?? [])])
)
export default AiGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function AiLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -10,6 +10,8 @@ import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLin
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'vecs'
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const ApiGuidePage = async ({ params }: { params: Params }) => {
const slug = ['api', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('api')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['api', ...(params.slug ?? [])])
)
export default ApiGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function ApiLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const AuthGuidePage = async ({ params }: { params: Params }) => {
const slug = ['auth', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('auth')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['auth', ...(params.slug ?? [])])
)
export default AuthGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const CronGuidePage = async ({ params }: { params: Params }) => {
const slug = ['cron', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('cron')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['cron', ...(params.slug ?? [])])
)
export default CronGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function CronLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const DatabaseGuidePage = async ({ params }: { params: Params }) => {
const slug = ['database', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('database')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['database', ...(params.slug ?? [])])
)
export default DatabaseGuidePage
export { generateStaticParams, generateMetadata }
@@ -18,6 +18,8 @@ import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'wrappers'
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function DatabaseLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const DeploymentGuidePage = async ({ params }: { params: Params }) => {
const slug = ['deployment', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('deployment')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['deployment', ...(params.slug ?? [])])
)
export default DeploymentGuidePage
export { generateStaticParams, generateMetadata }
@@ -11,6 +11,8 @@ import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'setup-cli'
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function DeploymentLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -18,6 +18,8 @@ import {
terraformDocsRepo,
} from '../terraformConstants'
export const dynamicParams = false
// Each external docs page is mapped to a local page
const pageMap = [
{
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const FunctionsGuidePage = async ({ params }: { params: Params }) => {
const slug = ['functions', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('functions')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['functions', ...(params.slug ?? [])])
)
export default FunctionsGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function FunctionsLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const GettingStartedGuidePage = async ({ params }: { params: Params }) => {
const slug = ['getting-started', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('getting-started')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['getting-started', ...(params.slug ?? [])])
)
export default GettingStartedGuidePage
export { generateStaticParams, generateMetadata }
@@ -23,7 +23,7 @@ ${content}
return (
<GuideTemplate
meta={{ title: `API Prompt: ${heading}` }}
meta={{ title: `AI Prompt: ${heading}` }}
content={content}
editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)}
/>
@@ -0,0 +1,20 @@
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
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() }
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
async function getPrompts() {
const prompts = await getAiPrompts()
return prompts.map(
(prompt) =>
({
name: prompt.heading,
url: `/guides/getting-started/ai-prompts/${prompt.filename}`,
}) as Partial<NavMenuSection>
)
}
@@ -5,12 +5,14 @@ import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
export const dynamicParams = false
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'pg_graphql'
@@ -133,7 +135,7 @@ const getContent = async ({ slug }: Params) => {
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
const response = await fetchRevalidatePerDay(
const response = await fetchRevalidatePerDay_TEMP_TESTING(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function GraphQlLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const IntegrationsGuidePage = async ({ params }: { params: Params }) => {
const slug = ['integrations', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('integrations')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['integrations', ...(params.slug ?? [])])
)
export default IntegrationsGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,39 @@
import { IS_PLATFORM } from 'common'
import { unstable_cache } from 'next/cache'
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
import Layout from '~/layouts/guides'
import { supabaseMisc } from '~/lib/supabaseMisc'
export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) {
const additionalNavItems = { integrations: await getPartners() }
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}
// Will need to turn on revalidation later, just turning it off for now so we
// can slowly turn things back on while monitoring usage
const getPartners = unstable_cache(getPartnersImpl, [], { revalidate: false })
async function getPartnersImpl() {
if (!IS_PLATFORM) return []
const { data, error } = await supabaseMisc()
.from('partners')
.select('slug, title')
.eq('approved', true)
.eq('type', 'technology')
.order('title')
if (error) {
console.error(new Error('Error fetching partners', { cause: error }))
}
const partnerNavItems = (data ?? []).map(
(partner) =>
({
name: partner.title,
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
}) as Partial<NavMenuSection>
)
return partnerNavItems
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const LocalDevelopmentGuidePage = async ({ params }: { params: Params }) => {
const slug = ['local-development', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('local-development')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['local-development', ...(params.slug ?? [])])
)
export default LocalDevelopmentGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function LocalDevelopmentLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const PlatformGuidePage = async ({ params }: { params: Params }) => {
const slug = ['platform', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('platform')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['platform', ...(params.slug ?? [])])
)
export default PlatformGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function PlatformLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const QueuesGuidePage = async ({ params }: { params: Params }) => {
const slug = ['queues', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('queues')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['queues', ...(params.slug ?? [])])
)
export default QueuesGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function QueuesLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const RealtimeGuidePage = async ({ params }: { params: Params }) => {
const slug = ['realtime', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('realtime')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['realtime', ...(params.slug ?? [])])
)
export default RealtimeGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function RealtimeLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const ResourcesGuidePage = async ({ params }: { params: Params }) => {
const slug = ['resources', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('resources')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['resources', ...(params.slug ?? [])])
)
export default ResourcesGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function ResourcesLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const SelfHostingGuidePage = async ({ params }: { params: Params }) => {
const slug = ['self-hosting', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('self-hosting')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['self-hosting', ...(params.slug ?? [])])
)
export default SelfHostingGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function SelfHostingLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const StorageGuidePage = async ({ params }: { params: Params }) => {
const slug = ['storage', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('storage')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['storage', ...(params.slug ?? [])])
)
export default StorageGuidePage
export { generateStaticParams, generateMetadata }
+5
View File
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function StorageLayout({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -0,0 +1,25 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
export const dynamicParams = false
type Params = { slug?: string[] }
const MonitoringTroubleshootingGuidePage = async ({ params }: { params: Params }) => {
const slug = ['telemetry', ...(params.slug ?? [])]
const data = await getGuidesMarkdown(slug)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams('telemetry')
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
getGuidesMarkdown(['telemetry', ...(params.slug ?? [])])
)
export default MonitoringTroubleshootingGuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,5 @@
import Layout from '~/layouts/guides'
export default async function Telemetry({ children }: { children: React.ReactNode }) {
return <Layout>{children}</Layout>
}
@@ -41,11 +41,14 @@ export default async function GlobalTroubleshootingPage() {
/>
<TroubleshootingListController />
<TroubleshootingFilterEmptyState />
<div id={TROUBLESHOOTING_CONTAINER_ID}>
<div id={TROUBLESHOOTING_CONTAINER_ID} className="@container/troubleshooting">
<h2 className="sr-only">Matching troubleshooting entries</h2>
<ul>
<ul className="grid @4xl/troubleshooting:grid-cols-[78%_15%_7%]">
{troubleshootingEntries.map((entry) => (
<li key={entry.data.database_id}>
<li
key={entry.data.database_id}
className="grid grid-cols-subgrid @4xl/troubleshooting:col-span-3"
>
<TroubleshootingPreview entry={entry} />
</li>
))}
@@ -5,12 +5,20 @@ const CostWarning = () => (
<Admonition type="warning">
<p>
To keep SMS sending costs under control, make sure you adjust your project&apos;s rate limits
and <Link href="/guides/auth/auth-captcha">configure CAPTCHA</Link>. See the{' '}
<Link href="/guides/platform/going-into-prod">Production Checklist</Link> to learn more.
and{' '}
<Link href="/guides/auth/auth-captcha">
<u>configure CAPTCHA</u>
</Link>
. See the{' '}
<Link href="/guides/platform/going-into-prod">
<u>Production Checklist</u>
</Link>{' '}
to learn more.
</p>
<br />
<p>
Some countries have special regulations for services that send SMS messages to users, for
example, India&apos;s TRAI DLT regulations. Remember to look up and follow the regulations of
Some countries have special regulations for services that send SMS messages to users, (e.g
India&apos;s TRAI DLT regulations). Remember to look up and follow the regulations of
countries where you operate.
</p>
</Admonition>
+3 -3
View File
@@ -13,6 +13,7 @@ import {
} from 'react'
import { type Database, useConstant, useIsLoggedIn } from 'common'
import { TelemetryActions } from 'common/telemetry-constants'
import { Button, cn } from 'ui'
import { IS_PLATFORM } from '~/lib/constants'
@@ -108,9 +109,8 @@ function Feedback({ className }: { className?: string }) {
function handleVote(response: Response) {
sendTelemetryEvent({
category: 'docs',
action: 'feedback_voted',
label: response,
action: TelemetryActions.DOCS_FEEDBACK_CLICKED,
properties: { response },
})
sendFeedbackVote(response)
dispatch({ event: 'VOTED', response })
+27 -25
View File
@@ -128,8 +128,6 @@ const GuidesTableOfContents = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hash, JSON.stringify(displayedList)])
if (!displayedList.length) return
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
return (
@@ -140,29 +138,33 @@ const GuidesTableOfContents = ({
</div>
)}
<Feedback key={pathname} />
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
On this page
</span>
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
{displayedList.map((item, i) => (
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
<a
href={`#${formatSlug(item.link)}`}
className="text-foreground-lighter hover:text-brand-link transition-colors"
>
{formatTOCHeader(removeAnchor(item.text)).map((x, index) => (
<Fragment key={index}>
{x.type === 'code' ? (
<code className="text-xs border rounded bg-muted">{x.value}</code>
) : (
x.value
)}
</Fragment>
))}
</a>
</li>
))}
</ul>
{displayedList.length > 0 && (
<>
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
On this page
</span>
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
{displayedList.map((item, i) => (
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
<a
href={`#${formatSlug(item.link)}`}
className="text-foreground-lighter hover:text-brand-link transition-colors"
>
{formatTOCHeader(removeAnchor(item.text)).map((x, index) => (
<Fragment key={index}>
{x.type === 'code' ? (
<code className="text-xs border rounded bg-muted">{x.value}</code>
) : (
x.value
)}
</Fragment>
))}
</a>
</li>
))}
</ul>
</>
)}
</div>
)
}
@@ -179,7 +179,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
const headers = new Headers()
if (code) {
const cookies = parse(request.headers.get('Cookie') ?? '')
const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
cookies: {
getAll() {
@@ -4,7 +4,7 @@ Go to [database.new](https://database.new) and create a new Supabase project.
When your project is up and running, go to the [Table Editor](https://supabase.com/dashboard/project/_/editor), create a new table and insert some data.
Alternatively, you can run the following snippet in your project's [SQL Editor](https://supabase.com/dashboard/project/_/sql/new). This will create a `countries` table with some sample data.
Alternatively, you can run the following snippet in your project's [SQL Editor](https://supabase.com/dashboard/project/_/sql/new). This will create a `instruments` table with some sample data.
</StepHikeCompact.Details>
@@ -12,18 +12,18 @@ Alternatively, you can run the following snippet in your project's [SQL Editor](
```sql SQL_EDITOR
-- Create the table
create table countries (
create table instruments (
id bigint primary key generated always as identity,
name text not null
);
-- Insert some sample data into the table
insert into countries (name)
insert into instruments (name)
values
('Canada'),
('United States'),
('Mexico');
('violin'),
('viola'),
('cello');
alter table countries enable row level security;
alter table instruments enable row level security;
```
</StepHikeCompact.Code>
@@ -37,8 +37,8 @@ Make the data in your table publicly readable by adding an RLS policy:
<StepHikeCompact.Code>
```sql SQL_EDITOR
create policy "public can read countries"
on public.countries
create policy "public can read instruments"
on public.instruments
for select to anon
using (true);
```
@@ -1,4 +1,4 @@
import { Clock, Heart, Server, SquareStack } from 'lucide-react'
import { Clock, Heart, Server, SquareStack, Telescope } from 'lucide-react'
import {
IconBranching,
@@ -87,6 +87,8 @@ function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, c
return <IconGitHub width={width} height={height} className={className} />
case 'support':
return <IconSupport width={width} height={height} className={className} />
case 'telemetry':
return <Telescope width={width} height={height} className={className} />
case 'troubleshooting':
return <IconTroubleshooting width={width} height={height} className={className} />
case 'contributing':
@@ -119,10 +119,15 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
level: 'platform',
},
{
label: 'Monitoring & Troubleshooting',
label: 'Telemetry',
icon: 'telemetry',
href: '/guides/telemetry',
level: 'telemetry',
},
{
label: 'Troubleshooting',
icon: 'troubleshooting',
href: '/guides/monitoring-troubleshooting',
level: 'troubleshooting',
href: '/guides/troubleshooting',
},
],
],
@@ -212,11 +217,6 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
label: 'Resources',
menuItems: [
[
/* {
label: 'Troubleshooting',
icon: 'contributing',
href: '/guides/troubleshooting',
}, */
{
label: 'Glossary',
icon: 'resources',
@@ -1322,7 +1322,7 @@ export const functions: NavMenuConstant = {
name: 'Guides',
url: undefined,
items: [
{ name: 'Managing dependencies', url: '/guides/functions/import-maps' },
{ name: 'Managing dependencies', url: '/guides/functions/dependencies' },
{
name: 'Managing environment variables',
url: '/guides/functions/secrets',
@@ -1359,6 +1359,10 @@ export const functions: NavMenuConstant = {
name: 'Running AI Models',
url: '/guides/functions/ai-models',
},
{
name: 'Wasm modules',
url: '/guides/functions/wasm',
},
{
name: 'Deploying with CI / CD pipelines',
url: '/guides/functions/cicd-workflow',
@@ -2091,71 +2095,35 @@ export const platform: NavMenuConstant = {
],
}
export const monitoring_troubleshooting: NavMenuConstant = {
icon: 'troubleshooting',
title: 'Monitor & Fix',
url: '/guides/monitoring-troubleshooting',
export const telemetry: NavMenuConstant = {
icon: 'telemetry',
title: 'Telemetry',
url: '/guides/telemetry',
items: [
{ name: 'Overview', url: '/guides/monitoring-troubleshooting' },
{ name: 'Overview', url: '/guides/telemetry' },
{
name: 'Logging & observability',
url: undefined,
items: [
{
name: 'Logging',
url: '/guides/monitoring-troubleshooting/logs',
url: '/guides/telemetry/logs',
},
{
name: 'Advanced log filtering',
url: '/guides/monitoring-troubleshooting/advanced-log-filtering',
url: '/guides/telemetry/advanced-log-filtering',
},
{
name: 'Log drains',
url: '/guides/monitoring-troubleshooting/log-drains',
url: '/guides/telemetry/log-drains',
},
{
name: 'Metrics',
url: '/guides/monitoring-troubleshooting/metrics',
url: '/guides/telemetry/metrics',
},
{
name: 'Sentry integration',
url: '/guides/monitoring-troubleshooting/sentry-monitoring',
},
],
},
{
name: 'Troubleshooting',
url: undefined,
items: [
{
name: 'HTTP and project issues',
url: '/guides/monitoring-troubleshooting/troubleshooting',
},
{
name: 'High disk IO consumption',
url: '/guides/monitoring-troubleshooting/exhaust-disk-io',
},
{
name: 'High CPU usage',
url: '/guides/monitoring-troubleshooting/exhaust-cpu',
},
{
name: 'High RAM usage',
url: '/guides/monitoring-troubleshooting/exhaust-ram',
},
{
name: 'High swap usage',
url: '/guides/monitoring-troubleshooting/exhaust-swap',
},
],
},
{
name: 'Status codes',
url: undefined,
items: [
{
name: 'HTTP status codes',
url: '/guides/monitoring-troubleshooting/http-status-codes',
url: '/guides/telemetry/sentry-monitoring',
},
],
},
@@ -19,7 +19,7 @@ enum MenuId {
Queues = 'queues',
Platform = 'platform',
Deployment = 'deployment',
MonitoringTroubleshooting = 'monitoring_troubleshooting',
Telemetry = 'telemetry',
Resources = 'resources',
SelfHosting = 'self_hosting',
Integrations = 'integrations',
@@ -92,7 +92,7 @@ const menus: Menu[] = [
type: 'guide',
},
{
id: MenuId.MonitoringTroubleshooting,
id: MenuId.Telemetry,
type: 'guide',
},
{
@@ -125,8 +125,8 @@ export const getMenuId = (pathname: string | null) => {
return MenuId.Integrations
case pathname.startsWith('local-development'):
return MenuId.LocalDevelopment
case pathname.startsWith('monitoring-troubleshooting'):
return MenuId.MonitoringTroubleshooting
case pathname.startsWith('telemetry'):
return MenuId.Telemetry
case pathname.startsWith('platform'):
return MenuId.Platform
case pathname.startsWith('queues'):
@@ -78,12 +78,20 @@ final data = await supabase.schema('myschema').from('todos').select();
```bash
# Append /rest/v1/ to your URL, and then use the table name as the route.
# Use Accept-Profile header for GET and HEAD.
# Use Content-Profile header for POST, PATCH, PUT and DELETE.
# for GET or HEAD request use Accept-Profile
curl '<SUPABASE_URL>/rest/v1/todos' \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Accept-Profile: myschema"
# for POST, PATCH, PUT and DELETE Request use Content-Profile
curl -X POST '<SUPABASE_URL>/rest/v1/todos' \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Content-Type: application/json" \
-H "Content-Profile: myschema" \
-d '{"column_name": "value"}'
```
</TabPanel>
@@ -133,7 +133,7 @@ If you're using PKCE flow, edit the Magic Link [email template](/docs/guides/aut
<h2>Magic Link</h2>
<p>Follow this link to login:</p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=magiclink">Log In</a></p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Log In</a></p>
```
At the `/auth/confirm` endpoint, exchange the hash for the session:
@@ -18,7 +18,7 @@ The templating system provides the following variables for use:
| Name | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=signup&redirect_to=https://example.com/path` . |
| `{{ .ConfirmationURL }}` | Contains the confirmation URL. For example, a signup confirmation URL would look like: `https://project-ref.supabase.co/auth/v1/verify?token={{ .TokenHash }}&type=email&redirect_to=https://example.com/path` . |
| `{{ .Token }}` | Contains a 6-digit One-Time-Password (OTP) that can be used instead of the `{{. ConfirmationURL }}` . |
| `{{ .TokenHash }}` | Contains a hashed version of the `{{ .Token }}`. This is useful for constructing your own email link in the email template. |
| `{{ .SiteURL }}` | Contains your application's Site URL. This can be configured in your project's [authentication settings](/dashboard/project/_/auth/url-configuration). |
@@ -10,8 +10,8 @@ Claims returned must conform to our specification. Supabase Auth will check for
These are the fields currently available on an access token:
Required Claims: `aud`, `exp`, `iat`, `sub`, `email`, `phone`, `role`, `aal`, `session_id`
Optional Claims: `jti`, `iss`, `nbf`, `app_metadata`, `user_metadata`, `amr`
Required Claims: `iss`, `aud`, `exp`, `iat`, `sub`, `role`, `aal`, `session_id`
Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`, `email`, `phone`
**Inputs**
@@ -205,8 +205,57 @@ Return these only if your hook processed the input without errors.
scrollable
size="small"
type="underlined"
defaultActiveId="add-admin-role"
defaultActiveId="minimal-jwt"
>
<TabPanel id="minimal-jwt" label="Minimal JWT">
Sometimes the size of the JWT can be a problem especially if you're using a [Server-Side Rendering framework](/docs/guides/auth/server-side). Common situations where the JWT can get too large include:
- The user has a particularly large name, email address or phone number
- The default JWT has too many claims coming from OAuth providers
- A large avatar URL is included
To lower the size of the JWT you can define a Custom Access Token hook like the one below which will instruct the Auth server to issue a JWT with only the listed claims. Check the documentation above on what JWT claims must be present and cannot be removed.
Refer to the [Postgres JSON functions](https://www.postgresql.org/docs/current/functions-json.html) on how to manipulate `jsonb` objects.
```sql
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
as $$
declare
original_claims jsonb;
new_claims jsonb;
claim text;
begin
original_claims = event->'claims';
new_claims = '{}'::jsonb;
foreach claim in array array[
-- add claims you want to keep here
'iss',
'aud',
'exp',
'iat',
'sub',
'role',
'aal',
'session_id'
] loop
if original_claims ? claim then
-- original_claims contains one of the listed claims, set it on new_claims
new_claims = jsonb_set(new_claims, array[claim], original_claims->claim);
end if;
end loop;
return jsonb_build_object('claims', new_claims);
end
$$;
```
</TabPanel>
<TabPanel id="add-admin-role" label="Add admin role">
You can allow registered admin users to perform restricted actions by granting an `admin` claim to their token.
@@ -3,7 +3,13 @@ title: 'Phone Login'
description: 'Learn about logging in to your platform using SMS one-time passwords.'
---
Phone Login is a method of authentication that allows users to log in to a website or application without using a password. The user authenticates through a one-time code sent via SMS.
Phone Login is a method of authentication that allows users to log in to a website or application without using a password. The user authenticates through a one-time password (OTP) sent via a channel (SMS or WhatsApp).
<Admonition type="info">
At this time, `WhatsApp` is only supported as a channel for the Twilio and Twilio Verify Providers.
</Admonition>
Users can also log in with their phones using Native Mobile Login with the built-in identity provider. For Native Mobile Login with Android and iOS, see the [Social Login guides](/docs/guides/auth/social-login).
@@ -86,10 +86,10 @@ For example, change the following:
```html
<!-- Old -->
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup">Confirm your mail</a>
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Confirm your mail</a>
<!-- New -->
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup"
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=email"
>Confirm your mail</a
>
```
@@ -401,7 +401,7 @@ If you have email confirmation turned on (the default), a new user will receive
Change the email template to support a server-side authentication flow.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
</StepHikeCompact.Details>
@@ -791,7 +791,7 @@ If you have email confirmation turned on (the default), a new user will receive
Change the email template to support a server-side authentication flow.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/api/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/api/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
</StepHikeCompact.Details>
@@ -928,9 +928,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
You can also fetch static data at build time using Supabase. Note that there's no session or user at build time, so the data will be the same for everyone who sees the page.
Add some countries data to your database by running the [Countries Quickstart](https://supabase.com/dashboard/project/_/sql/quickstarts) in the dashboard.
Add some colors data to your database by running the [Colors Quickstart](https://supabase.com/dashboard/project/_/sql/quickstarts) in the dashboard.
Then fetch the countries data using `getStaticProps` with the client from `@/utils/supabase/static-props.ts`.
Then fetch the colors data using `getStaticProps` with the client from `@/utils/supabase/static-props.ts`.
</StepHikeCompact.Details>
@@ -946,7 +946,7 @@ export default function PublicPage({ data }: { data?: any[] }) {
export async function getStaticProps() {
const supabase = createClient()
const { data, error } = await supabase.from('countries').select()
const { data, error } = await supabase.from('colors').select()
if (error || !data) {
return { props: {} }
@@ -308,7 +308,7 @@ Create your first page. This example page calls Supabase from the server to get
This is an example of a public page that uses publicly readable data.
To populate your database, run the [countries quickstart](/dashboard/project/_/sql/quickstarts) from your dashboard.
To populate your database, run the [colors quickstart](/dashboard/project/_/sql/quickstarts) from your dashboard.
</StepHikeCompact.Details>
@@ -320,21 +320,21 @@ To populate your database, run the [countries quickstart](/dashboard/project/_/s
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ locals: { supabase } }) => {
const { data: countries } = await supabase.from('countries').select('name').limit(5).order('name')
return { countries: countries ?? [] }
const { data: colors } = await supabase.from('colors').select('name').limit(5).order('name')
return { colors: colors ?? [] }
}
```
```svelte src/routes/+page.svelte
<script>
let { data } = $props()
let { countries } = $derived(data)
let { colors } = $derived(data)
</script>
<h1>Welcome to Supabase!</h1>
<ul>
{#each countries as country}
<li>{country.name}</li>
{#each colors as color}
<li>{color.name}</li>
{/each}
</ul>
```
@@ -24,7 +24,7 @@ Setting up OAuth with Azure consists of four broad steps:
- Under Microsoft Entra ID, select _App registrations_ in the side panel and select _New registration._
- Choose a name and select your preferred option for the supported account types.
- Specify a _Web_ _Redirect URI_. It should should look like this: `https://<project-ref>.supabase.co/auth/v1/callback`
- Specify a _Web_ _Redirect URI_. It should look like this: `https://<project-ref>.supabase.co/auth/v1/callback`
- Finally, select _Register_ at the bottom of the screen.
![Register an application.](/docs/img/guides/auth-azure/azure-register-app.png)
@@ -80,7 +80,7 @@ Future<void> signInWithGithub() async {
</TabPanel>
<TabPanel id="swift" label="Swift">
When your user signs in, call [signInWithWithOAuth](/docs/reference/swift/auth-signinwithoauth) with `.github` as the `Provider`:
When your user signs in, call [signInWithOAuth](/docs/reference/swift/auth-signinwithoauth) with `.github` as the `Provider`:
```swift
func signInWithGithub() async throws {
@@ -778,3 +778,11 @@ chrome.identity.launchWebAuthFlow(
</TabPanel>
</Tabs>
## Google consent screen
![Google Consent Screen](/docs/img/guides/auth-google/auth-google-consent-screen.png)
By default, the Google consent screen shows the root domain of the callback URL, where Google will send the authentication response. With Supabase Auth, it is your Supabase project's domain `(https://<your-project-ref>.supabase.co)`.
If that is not preferable, you can use a [Custom Domain](https://supabase.com/docs/guides/platform/custom-domains) with your Supabase project. You can use it as your project's domain when creating the Supabase client in your application and initiating the authentication flow. It will then show up in the Google consent screen. If you want your app name and the logo on the consent screen, [you must submit your app to Google for verification](https://support.google.com/cloud/answer/10311615).
+10 -14
View File
@@ -4,19 +4,13 @@ title: 'Firebase Auth'
subtitle: 'Use Firebase Auth with your Supabase project'
---
<Admonition type="note">
Creating a Firebase Auth third-party integration with your Supabase project is currently limited to a private alpha release. We are still trying to improve the developer experience around securing your project. [Register your interest in Third-Party Auth with Firebase](https://forms.supabase.com/third-party-auth-with-firebase) and the team will reach out to you.
</Admonition>
Firebase Auth can be used as a third-party authentication provider alongside Supabase Auth, or standalone, with your Supabase project.
## Getting started
1. First you need to add an integration to connect your Supabase project with your Firebase project. You will need to get the Project ID in the [Firebase Console](https://console.firebase.google.com/u/0/project/_/settings/general).
2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth).
3. If you are using Third Party Auth in local development, create and attach restrictive RLS policies to all tables in your public schema, Storage and Realtime to prevent unauthorized access from unrelated Firebase projects.
3. If you are using Third Party Auth when self hosting, create and attach restrictive RLS policies to all tables in your public schema, Storage and Realtime to **prevent unauthorized access from unrelated Firebase projects**.
4. Assign the `role: 'authenticated'` [custom user claim](https://firebase.google.com/docs/auth/admin/custom-claims) to all your users.
5. Finally set up the Supabase client in your application.
@@ -33,7 +27,7 @@ import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://<supabase-project>.supabase.co', 'SUPABASE_ANON_KEY', {
accessToken: async () => {
;(await firebase.auth().currentUser?.getIdToken(/* forceRefresh */ false)) ?? null
return await firebase.auth().currentUser?.getIdToken(/* forceRefresh */ false)) ?? null
},
})
```
@@ -123,19 +117,21 @@ enabled = true
project_id = "<id>"
```
## Adding an extra layer of security to your project's RLS policies
## Adding an extra layer of security to your project's RLS policies (self-hosting only)
<Admonition type="note">
<Admonition type="caution">
While Third-Party Auth with Firebase is in a private alpha, it is strongly recommended to follow this guide closely. Once it becomes generally available this method would not be necessary to secure your project against unauthorized access from other Firebase projects.
**Please follow this section carefully to prevent unauthorized access to your project's data when self-hosting.**
You should also follow this best practice if self-hosting.
When using the Supabase hosted platform, following this step is optional.
</Admonition>
Firebase Auth uses a single set of JWT signing keys for all projects. This means that JWTs issued from an unrelated Firebase project to yours could access data in your Supabase project. To guard against this, creating and maintaining the following RLS policies for **all of your tables in the `public` schema** is very important. You should also attach this policy to [Storage](/docs/guides/storage/security/access-control) buckets or [Realtime](/docs/guides/realtime/authorization) channels.
Firebase Auth uses a single set of JWT signing keys for all projects. This means that JWTs issued from an unrelated Firebase project to yours could access data in your Supabase project.
To achieve this we recommend using a [restrictive Postgres Row-Level Security policy](https://www.postgresql.org/docs/current/sql-createpolicy.html).
When using the Supabase hosted platform, JWTs coming from Firebase project IDs you have not registered will be rejected before they reach your database. When self-hosting implementing this mechanism is your responsibility. An easy way to guard against this is to create and maintain the following RLS policies for **all of your tables in the `public` schema**. You should also attach this policy to [Storage](/docs/guides/storage/security/access-control) buckets or [Realtime](/docs/guides/realtime/authorization) channels.
It's recommended you use a [restrictive Postgres Row-Level Security policy](https://www.postgresql.org/docs/current/sql-createpolicy.html).
Restrictive RLS policies differ from regular (or permissive) policies in that they use the `as restrictive` clause when being defined. They do not grant permissions, but rather restrict any existing or future permissions. They're great for cases like this where the technical limitations of Firebase Auth remain separate from your app's logic.
@@ -120,9 +120,12 @@ The following parameters are available for overrides:
1. [max_worker_processes](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-MAX-WORKER-PROCESSES) (CLI only)
1. [session_replication_role](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE)
1. [shared_buffers](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS) (CLI only)
1. [statement_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT)
1. [track_activity_query_size](https://www.postgresql.org/docs/current/runtime-config-statistics.html#GUC-TRACK-ACTIVITY-QUERY-SIZE)
1. [track_commit_timestamp](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP)
1. [wal_keep_size](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-KEEP-SIZE) (CLI only)
1. [wal_sender_timeout](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT) (CLI only)
1. [work_mem](https://www.postgresql.org/docs/current/runtime-config-replication.html#GUC-WAL-SENDER-TIMEOUT)
1. [work_mem](https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM)
#### Managing Postgres configuration with the CLI
@@ -148,6 +151,22 @@ postgres-config update --config max_parallel_workers=3 \
--replace-existing-overrides
```
To delete specific configuration overrides, use the `postgres-config delete` command:
```bash
supabase --experimental \
--project-ref <project-ref> \
postgres-config delete --config shared_buffers,work_mem
```
By default, this might restart the database after deleting the configurations. You can use the `--no-restart` flag to prevent the restart:
```bash
supabase --experimental \
--project-ref <project-ref> \
postgres-config delete --config shared_buffers --no-restart
```
### Resetting to default config
To reset a setting to its default value at the database level:
@@ -168,6 +187,6 @@ alter role "<role_name>" set "<setting_name>" to default;
### Considerations
1. Changes through the CLI must restart the database and will cause momentary disruption to existing database connections; in most cases this should not take more than a few seconds. However, you can use the --no-restart flag to bypass the restart and keep the connections intact. Keep in mind that this depends on the specific configuration changes you’re making. if the change requires a restart, using the --no-restart flag will prevent the restart but you won’t see those changes take effect until a restart is manually triggered.
1. Changes through the CLI might restart the database causing momentary disruption to existing database connections; in most cases this should not take more than a few seconds. However, you can use the --no-restart flag to bypass the restart and keep the connections intact. Keep in mind that this depends on the specific configuration changes you're making. if the change requires a restart, using the --no-restart flag will prevent the restart but you won't see those changes take effect until a restart is manually triggered.
1. Custom Postgres Config will always override the default optimizations generated by Supabase. When changing compute add-ons, you should also review and update your custom Postgres Config to ensure they remain compatible and effective with the updated compute.
1. Some parameters (e.g. `wal_keep_size`) can increase disk utilization, triggering disk expansion, which in turn can lead to [increases in your bill](/docs/guides/platform/compute-add-ons#disk-io).
@@ -14,7 +14,7 @@ subtitle: 'Debug slow-running queries using the Postgres execution planner.'
{/* prettier-ignore */}
```sql
-- enable explain
alter role authenticator
alter role authenticator
set pgrst.db_plan_enabled to 'true';
-- reload the config
@@ -28,28 +28,28 @@ To get the execution plan of a query, you can chain the `explain()` method to a
{/* prettier-ignore */}
```ts
const { data, error } = await supabase
.from('countries')
.from('instruments')
.select()
.explain()
```
### Example data
To illustrate, consider the following setup of a `countries` table:
To illustrate, consider the following setup of a `instruments` table:
{/* prettier-ignore */}
```sql
create table countries (
create table instruments (
id int8 primary key,
name text
);
insert into countries
insert into books
(id, name)
values
(1, 'Afghanistan'),
(2, 'Albania'),
(3, 'Algeria');
(1, 'violin'),
(2, 'viola'),
(3, 'cello');
```
### Expected response
@@ -60,7 +60,7 @@ The response would typically look like this:
```markdown
Aggregate (cost=33.34..33.36 rows=1 width=112)
-> Limit (cost=0.00..18.33 rows=1000 width=40)
-> Seq Scan on countries (cost=0.00..22.00 rows=1200 width=40)
-> Seq Scan on instruments (cost=0.00..22.00 rows=1200 width=40)
```
By default, the execution plan is returned in TEXT format. However, you can also retrieve it as JSON by specifying the `format` parameter.
@@ -97,11 +97,11 @@ To disable the `explain()` method after use, execute the following SQL commands:
{/* prettier-ignore */}
```sql
-- disable explain
alter role authenticator
alter role authenticator
set pgrst.db_plan_enabled to 'false';
-- if you used the above pre-request
alter role authenticator
alter role authenticator
set pgrst.db_pre_request to '';
-- reload the config
@@ -4,4 +4,4 @@ title: 'pgmq: Queues'
description: 'pgmq: Managed queues in Postgres'
---
See the [Supabase Cron docs](/docs/guides/queues).
See the [Supabase Queues docs](/docs/guides/queues).
@@ -203,15 +203,22 @@ Sorting datasets from closest to farthest, sometimes called nearest-neighbor sor
```sql
create or replace function nearby_restaurants(lat float, long float)
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float, dist_meters float)
set search_path = ''
language sql
as $$
select id, name, st_y(location::geometry) as lat, st_x(location::geometry) as long, st_distance(location, st_point(long, lat)::geography) as dist_meters
select id, name, gis.st_y(location::gis.geometry) as lat, gis.st_x(location::gis.geometry) as long, gis.st_distance(location, gis.st_point(long, lat)::gis.geography) as dist_meters
from public.restaurants
order by location <-> st_point(long, lat)::geography;
order by location operator(gis.<->) gis.st_point(long, lat)::gis.geography;
$$;
```
You can call this function from your client using `rpc()` like this:
Before being able to call this function from our client we need to grant access to our `gis` schema:
```sql
grant usage on schema gis to anon, authenticated;
```
Now you can call this function from your client using `rpc()` like this:
<Tabs
scrollable
@@ -321,11 +328,12 @@ When you are working on a map-based application where the user scrolls through y
```sql
create or replace function restaurants_in_view(min_lat float, min_long float, max_lat float, max_long float)
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float)
set search_path to ''
language sql
as $$
select id, name, st_y(location::geometry) as lat, st_x(location::geometry) as long
select id, name, gis.st_y(location::gis.geometry) as lat, gis.st_x(location::gis.geometry) as long
from public.restaurants
where location && ST_SetSRID(ST_MakeBox2D(ST_Point(min_long, min_lat), ST_Point(max_long, max_lat)), 4326)
where location operator(gis.&&) gis.ST_SetSRID(gis.ST_MakeBox2D(gis.ST_Point(min_long, min_lat), gis.ST_Point(max_long, max_lat)), 4326)
$$;
```
@@ -8,7 +8,7 @@ The data APIs automatically detect relationships between Postgres tables. Since
## One-to-many joins
Let's use an example database that stores `countries` and `cities`:
Let's use an example database that stores `orchestral_sections` and `instruments`:
<Tabs
scrollable
@@ -19,50 +19,50 @@ Let's use an example database that stores `countries` and `cities`:
>
<TabPanel id="table" label="Tables">
**Countries**
**Orchestral sections**
| `id` | `name` |
| ---- | -------------- |
| 1 | United Kingdom |
| 2 | United States |
| `id` | `name` |
| ---- | --------- |
| 1 | strings |
| 2 | woodwinds |
**Cities**
**Instruments**
| `id` | `name` | `country_id` |
| ---- | ----------- | ------------ |
| 1 | London | 1 |
| 2 | Manchester | 1 |
| 3 | Los Angeles | 2 |
| 4 | New York | 2 |
| `id` | `name` | `section_id` |
| ---- | ------ | ------------ |
| 1 | violin | 1 |
| 2 | viola | 1 |
| 3 | flute | 2 |
| 4 | oboe | 2 |
</TabPanel>
<TabPanel id="SQL" label="SQL">
```sql
create table countries (
create table orchestral_sections (
"id" serial primary key,
"name" text
);
insert into countries
insert into orchestral_sections
(id, name)
values
(1, 'United Kingdom'),
(2, 'United States');
(1, 'strings'),
(2, 'woodwinds');
create table cities (
create table instruments (
"id" serial primary key,
"name" text,
"country_id" int references "countries"
"section_id" int references "orchestral_sections"
);
insert into cities
(name, country_id)
insert into instruments
(name, section_id)
values
('London', 1),
('Manchester', 1),
('Los Angeles', 2),
('New York', 2);
('violin', 1),
('viola', 1),
('flute', 2),
('oboe', 2);
```
</TabPanel>
@@ -80,10 +80,10 @@ The APIs will automatically detect relationships based on the foreign keys:
<TabPanel id="js" label="JavaScript">
```js
const { data, error } = await supabase.from('countries').select(`
id,
name,
cities ( id, name )
const { data, error } = await supabase.from('orchestral_sections').select(`
id,
name,
instruments ( id, name )
`)
```
@@ -93,66 +93,66 @@ const { data, error } = await supabase.from('countries').select(`
These helper types provide the result types from any query, including nested types for database joins.
Given the following schema with a relation between cities and countries:
Given the following schema with a relation between orchestral sections and instruments:
```sql
create table countries (
create table orchestral_sections (
"id" serial primary key,
"name" text
);
create table cities (
create table instruments (
"id" serial primary key,
"name" text,
"country_id" int references "countries"
"section_id" int references "orchestral_sections"
);
```
We can get the nested `CountriesWithCities` type like this:
We can get the nested `SectionsWithInstruments` type like this:
```ts
import { QueryResult, QueryData, QueryError } from '@supabase/supabase-js'
const countriesWithCitiesQuery = supabase.from('countries').select(`
const sectionsWithInstrumentsQuery = supabase.from('orchestral_sections').select(`
id,
name,
cities (
instruments (
id,
name
)
`)
type CountriesWithCities = QueryData<typeof countriesWithCitiesQuery>
type SectionsWithInstruments = QueryData<typeof sectionsWithInstrumentsQuery>
const { data, error } = await countriesWithCitiesQuery
const { data, error } = await sectionsWithInstrumentsQuery
if (error) throw error
const countriesWithCities: CountriesWithCities = data
const sectionsWithInstruments: SectionsWithInstruments = data
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
final data = await supabase.from('countries').select('id, name, cities(id, name)');
final data = await supabase.from('orchestral_sections').select('id, name, instruments(id, name)');
```
</TabPanel>
<TabPanel id="swift" label="Swift">
```swift
struct Country: Codable {
struct OrchestralSection: Codable {
let id: Int
let name: String
let cities: [City]
let instruments: [Instrument]
struct City: Codable {
struct Instrument: Codable {
let id: Int
let name: String
}
}
let countries: [Country] = try await supabase
.from("countries")
.select("id, name, cities(id, name)")
let orchestralSections: [OrchestralSection] = try await supabase
.from("orchestral_sections")
.select("id, name, instruments(id, name)")
.execute()
.value
```
@@ -161,14 +161,14 @@ let countries: [Country] = try await supabase
<TabPanel id="kotlin" label="Kotlin">
```kotlin
val data = supabase.from("countries").select(Columns.raw("id, name, cities(id, name)"))
val data = supabase.from("orchestral_sections").select(Columns.raw("id, name, instruments(id, name)"))
```
</TabPanel>
<TabPanel id="python" label="Python">
```python
data = supabase.from_('countries').select('id, name, cities(id, name)').execute()
data = supabase.from_('orchestral_sections').select('id, name, instruments(id, name)').execute()
```
</TabPanel>
@@ -177,13 +177,13 @@ data = supabase.from_('countries').select('id, name, cities(id, name)').execute(
```javascript
const Query = `
query {
countriesCollection {
orchestral_sectionsCollection {
edges {
node {
id
name
cities {
id,
name
instruments {
id,
name
}
}
@@ -197,7 +197,7 @@ const Query = `
<TabPanel id="url" label="URL">
```bash
GET https://[REF].supabase.co/rest/v1/countries?select=id,name,cities(id,name)
GET https://[REF].supabase.co/rest/v1/orchestral_sections?select=id,name,instruments(id,name)
```
</TabPanel>
@@ -238,8 +238,8 @@ In these cases you don't need to explicitly define the joining table (members).
```js
const { data, error } = await supabase.from('teams').select(`
id,
team_name,
id,
team_name,
users ( id, name )
`)
```
@@ -387,7 +387,7 @@ const { data, error } = await supabase.from('shifts').select(
badge_scan_time
),
end_scan:scans!scan_id_end (
id,
id,
user_id,
badge_scan_time
)
@@ -61,7 +61,7 @@ The default role timeouts are:
- `anon`: 3s
- `authenticated`: 8s
- `service_role`: none (capped by API to be 60s)
- `service_role`: none (defaults to the `authenticator` role's 8s timeout if unset)
- `postgres`: none (capped by default global timeout to be 2min)
Run the following query to change a role's timeout:
+1 -1
View File
@@ -140,7 +140,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t
In your .env file, set the DATABASE_URL variable to your connection string
```text .env
# Used for Prisma Migrations and within your application
DATABASE_URL="postgres://[DB-USER].[PROJECT-REF]:[PRISMA-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres"
DATABASE_URL="postgres://[DB-USER].[PROJECT-REF]:[PRISMA-PASSWORD]@[DB-REGION].pooler.supabase.com:5432/postgres"
```
Change your string's `[DB-USER]` to `prisma` and add the password you created in step 1
@@ -10,12 +10,6 @@ To ensure that queries return the expected data, RLS policies are correctly appl
- Secondly, you can test through the Supabase CLI, which is a more low-level approach where you write tests in SQL.
# Testing through the Supabase client
A good starting point are the tests written for the Supabase client itself. For example, [here](https://github.com/supabase/supabase/tree/master/tests/features/javascript) are the tests for the Supabase JS client written with TypeScript and Jest.
Keep in mind that the tests (which you'll likely integrate into your CI) need to run with the same DB setup that you're running locally. That means you need to make all changes to the database exclusively through migrations (not manually).
# Testing using the Supabase CLI
You can use the Supabase CLI to test your database. The minimum required version of the CLI is [v1.11.4](https://github.com/supabase/cli/releases). To get started:
Loaded 100 of 1042 files, more files were not shown because too many files have changed in this diff. Show more