diff --git a/.github/workflows/docs-last-changed.yml b/.github/workflows/docs-last-changed.yml
index 882e7847367..5025e4b0d10 100644
--- a/.github/workflows/docs-last-changed.yml
+++ b/.github/workflows/docs-last-changed.yml
@@ -19,7 +19,7 @@ jobs:
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
- SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
+ SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- name: Check out repo
diff --git a/.github/workflows/docs-lint-v2-comment.yml b/.github/workflows/docs-lint-v2-comment.yml
index 89c037c42fa..7194e9e2181 100644
--- a/.github/workflows/docs-lint-v2-comment.yml
+++ b/.github/workflows/docs-lint-v2-comment.yml
@@ -1,4 +1,4 @@
-name: Comment on external PRs with linter results
+name: docs_lint_comment_external
# This is a continuation of ./docs-lint-v2.yml, to write comments on external
# PRs.
@@ -11,7 +11,7 @@ name: Comment on external PRs with linter results
on:
workflow_run:
- workflows: ['[Docs] Lint v2']
+ workflows: [docs_lint]
types:
- completed
@@ -21,7 +21,7 @@ permissions:
jobs:
comment_on_pr:
runs-on: ubuntu-latest
- if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
+ if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
steps:
- id: download_artifact
name: 'Download artifact'
diff --git a/.github/workflows/docs-lint-v2.yml b/.github/workflows/docs-lint-v2.yml
index cf543f15f1e..4ceb1f82102 100644
--- a/.github/workflows/docs-lint-v2.yml
+++ b/.github/workflows/docs-lint-v2.yml
@@ -1,4 +1,4 @@
-name: '[Docs] Lint v2'
+name: docs_lint
# Runs the docs linter on PRs that edit docs content.
# There are two branches of this workflow for internal and external PRs, due
@@ -102,3 +102,6 @@ jobs:
with:
name: lint_results
path: __github_actions__pr/
+ - name: fail if linter fails (external)
+ if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
+ run: exit 1
diff --git a/.github/workflows/docs-sync-troubleshooting.yml b/.github/workflows/docs-sync-troubleshooting.yml
index f0d2ec07116..52c0d793e7c 100644
--- a/.github/workflows/docs-sync-troubleshooting.yml
+++ b/.github/workflows/docs-sync-troubleshooting.yml
@@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
env:
- SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
- SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
- SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
+ DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
+ DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
+ DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
diff --git a/.github/workflows/search.yml b/.github/workflows/search.yml
index 03de1b2e7aa..9d5aa59959a 100644
--- a/.github/workflows/search.yml
+++ b/.github/workflows/search.yml
@@ -29,12 +29,14 @@ jobs:
runs-on: ubuntu-latest
env:
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
+ DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
+ DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
+ NEXT_PUBLIC_MISC_URL: ${{ secrets.NEXT_PUBLIC_MISC_URL}}
+ NEXT_PUBLIC_MISC_ANON_KEY: ${{ secrets.NEXT_PUBLIC_MISC_ANON_KEY }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
- SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
- SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
- SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
- SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- name: Check out repo
@@ -58,10 +60,6 @@ jobs:
- name: Download dependencies
run: pnpm i
- # Need the miscellaneous use API, which is available publicly (by design) in www
- - name: Copy environment variables
- run: cp apps/www/.env.local.example apps/docs/.env
-
- name: Update embeddings
working-directory: ./apps/docs
if: ${{ !inputs.refresh }}
diff --git a/apps/docs/.env.development b/apps/docs/.env.development
index 7c41f0565e4..f5e09eed460 100644
--- a/apps/docs/.env.development
+++ b/apps/docs/.env.development
@@ -2,5 +2,17 @@
# Only publicly available environment variables should be listed here.
# Do not add secrets here. Use .env.local instead.
+NEXT_PUBLIC_SITE_URL="http://localhost:3001"
+NEXT_PUBLIC_BASE_PATH="/docs"
+
+# Whether to enable features available only on Supabase's own hosted services
+# Setting this to true requires certain secret keys
+NEXT_PUBLIC_IS_PLATFORM="false"
+
+# Supabase project containing integration information
NEXT_PUBLIC_MISC_URL="https://obuldanrptloktxcffvn.supabase.co"
NEXT_PUBLIC_MISC_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTg2MTQ2ODUsImV4cCI6MjAzNDE5MDY4NX0.NFt49g6DFkc1X5khCzN5p01iAVo2TMxlx88cY1V0E2M"
+
+# Supabase project containing docs content information
+NEXT_PUBLIC_SUPABASE_URL="https://xguihxuzqibwxjnimxev.supabase.co"
+NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTgzNzc1MTgsImV4cCI6MjAzMzk1MzUxOH0.aIqjQ9V7djMxYit-DT1fYNV_VWMHSqldh_18XfX2_BE"
diff --git a/apps/docs/.env.example b/apps/docs/.env.example
deleted file mode 100644
index 735dd88086d..00000000000
--- a/apps/docs/.env.example
+++ /dev/null
@@ -1,14 +0,0 @@
-# PUBLIC
-NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
-NEXT_PUBLIC_SUPABASE_ANON_KEY=
-
-# PRIVATE
-OPENAI_API_KEY=
-SUPABASE_SERVICE_ROLE_KEY=
-SEARCH_GITHUB_APP_ID=
-SEARCH_GITHUB_APP_INSTALLATION_ID=
-SEARCH_GITHUB_APP_PRIVATE_KEY=
-
-# Set IS_PLATFORM to true if you have the api keys and urls
-NEXT_PUBLIC_IS_PLATFORM=false
-NEXT_PUBLIC_LOCAL_SUPABASE=false
diff --git a/apps/docs/.eslintrc.js b/apps/docs/.eslintrc.cjs
similarity index 100%
rename from apps/docs/.eslintrc.js
rename to apps/docs/.eslintrc.cjs
diff --git a/apps/docs/app/api/crawlers/route.ts b/apps/docs/app/api/crawlers/route.ts
index 0aae261baea..253e6648cb1 100644
--- a/apps/docs/app/api/crawlers/route.ts
+++ b/apps/docs/app/api/crawlers/route.ts
@@ -85,7 +85,6 @@ function htmlShell(
`` +
`` +
`` +
'' +
diff --git a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap
index c668a0b5208..5c0bac58a6a 100644
--- a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap
+++ b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap
@@ -131,6 +131,9 @@ type RootQueryType {
"""Search the Supabase docs for content matching a query string"""
searchDocs(query: String!, limit: Int): SearchResultCollection
+
+ """Get the details of an error code returned from a Supabase service"""
+ error(code: String!, service: Service!): Error
}
"""A collection of search results containing content from Supabase docs"""
@@ -149,5 +152,30 @@ type SearchResultCollection {
type SearchResultEdge {
"""The SearchResult at the end of the edge"""
node: SearchResult!
+}
+
+"""An error returned by a Supabase service"""
+type Error {
+ """
+ The unique code identifying the error. The code is stable, and can be used for string matching during error handling.
+ """
+ code: String!
+
+ """The Supabase service that returns this error."""
+ service: Service!
+
+ """The HTTP status code returned with this error."""
+ httpStatusCode: Int
+
+ """
+ A human-readable message describing the error. The message is not stable, and should not be used for string matching during error handling. Use the code instead.
+ """
+ message: String
+}
+
+enum Service {
+ AUTH
+ REALTIME
+ STORAGE
}"
`;
diff --git a/apps/docs/app/api/graphql/route.test.ts b/apps/docs/app/api/graphql/route.test.ts
index 8b464e23f9d..bcd6633e9bf 100644
--- a/apps/docs/app/api/graphql/route.test.ts
+++ b/apps/docs/app/api/graphql/route.test.ts
@@ -3,15 +3,11 @@ import { POST } from './route'
describe('/api/graphql basic error statuses', () => {
beforeAll(() => {
- vi.mock('server-only', () => {
- return {}
- })
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterAll(() => {
vi.restoreAllMocks()
- vi.doUnmock('server-only')
})
beforeEach(() => {
diff --git a/apps/docs/app/api/graphql/tests/errors.test.ts b/apps/docs/app/api/graphql/tests/errors.test.ts
new file mode 100644
index 00000000000..92972b850fb
--- /dev/null
+++ b/apps/docs/app/api/graphql/tests/errors.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it, vi } from 'vitest'
+import { POST } from '../route'
+
+describe('/api/graphql error', () => {
+ it('returns the error matching given error code and service', async () => {
+ const errorQuery = `
+ query {
+ error(code: "test_code", service: AUTH) {
+ code
+ service
+ httpStatusCode
+ message
+ }
+ }
+ `
+ const request = new Request('http://localhost/api/graphql', {
+ method: 'POST',
+ body: JSON.stringify({ query: errorQuery }),
+ })
+
+ const result = await POST(request)
+ const {
+ data: { error },
+ errors,
+ } = await result.json()
+
+ expect(errors).toBeUndefined()
+ expect(error.code).toBe('test_code')
+ expect(error.service).toBe('AUTH')
+ expect(error.httpStatusCode).toBe(500)
+ expect(error.message).toBe('This is a test error message')
+ })
+
+ it('returns error if no matching error exists', async () => {
+ vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ const errorQuery = `
+ query {
+ error(code: "nonexistent_code", service: AUTH) {
+ code
+ service
+ httpStatusCode
+ message
+ }
+ }
+ `
+ const request = new Request('http://localhost/api/graphql', {
+ method: 'POST',
+ body: JSON.stringify({ query: errorQuery }),
+ })
+
+ const result = await POST(request)
+ const {
+ data: { error },
+ errors,
+ } = await result.json()
+
+ expect(error).toBe(null)
+ expect(errors).toHaveLength(1)
+ expect(errors[0].message).toMatch(/not found/i)
+
+ vi.restoreAllMocks()
+ })
+})
diff --git a/apps/docs/app/api/utils.ts b/apps/docs/app/api/utils.ts
index a2e87017713..949947abce5 100644
--- a/apps/docs/app/api/utils.ts
+++ b/apps/docs/app/api/utils.ts
@@ -45,6 +45,24 @@ export class InvalidRequestError extends
}
}
+export class NoDataError extends ApiError {
+ constructor(message: string, source?: unknown, details?: Details) {
+ super(`Data not found: ${message}`, source, details)
+ }
+
+ isPrivate() {
+ return false
+ }
+
+ isUserError() {
+ return true
+ }
+
+ statusCode() {
+ return 404
+ }
+}
+
export function convertUnknownToApiError(error: unknown): ApiError {
return new ApiError('Unknown error', error)
}
diff --git a/apps/docs/app/guides/database/database-advisors/page.tsx b/apps/docs/app/guides/database/database-advisors/page.tsx
index 184c8fee410..ce95941cf19 100644
--- a/apps/docs/app/guides/database/database-advisors/page.tsx
+++ b/apps/docs/app/guides/database/database-advisors/page.tsx
@@ -1,15 +1,15 @@
import { Octokit } from '@octokit/core'
-import { capitalize } from 'lodash'
+import { capitalize } from 'lodash-es'
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import rehypeSlug from 'rehype-slug'
import { Heading } from 'ui'
-import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
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 { Tabs, TabPanel } from '~/features/ui/Tabs'
+import { TabPanel, Tabs } from '~/features/ui/Tabs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
diff --git a/apps/docs/components/Feedback/Feedback.utils.ts b/apps/docs/components/Feedback/Feedback.utils.ts
index 3337d2f57ee..c93ebb55715 100644
--- a/apps/docs/components/Feedback/Feedback.utils.ts
+++ b/apps/docs/components/Feedback/Feedback.utils.ts
@@ -1,4 +1,4 @@
-import { pick } from 'lodash'
+import { pick } from 'lodash-es'
/**
* Gets the Notion team to send feedback to based on the pathname.
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index 0dcb96f727b..641b40d937d 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -119,7 +119,7 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
level: 'platform',
},
{
- label: 'Security',
+ label: 'Security & Compliance',
icon: 'security',
href: '/guides/security',
level: 'security',
@@ -917,10 +917,6 @@ export const database: NavMenuConstant = {
name: 'Database configuration',
url: '/guides/database/postgres/configuration',
},
- {
- name: 'Managing database replication',
- url: '/guides/database/replication',
- },
{
name: 'Query optimization',
url: '/guides/database/query-optimization',
@@ -960,6 +956,22 @@ export const database: NavMenuConstant = {
},
ormQuickstarts,
guiQuickstarts,
+ {
+ name: 'Database replication',
+ url: undefined,
+ items: [
+ { name: 'Overview', url: '/guides/database/replication' },
+ {
+ name: 'Setting up replication',
+ url: '/guides/database/replication/setting-up-replication',
+ },
+ {
+ name: 'Monitoring replication',
+ url: '/guides/database/replication/monitoring-replication',
+ },
+ { name: 'FAQ', url: '/guides/database/replication/faq' },
+ ],
+ },
{
name: 'Extensions',
url: undefined,
@@ -2080,6 +2092,19 @@ export const security: NavMenuConstant = {
{ name: 'HIPAA', url: '/guides/security/hipaa-compliance' },
],
},
+ {
+ name: 'Guides',
+ url: undefined,
+ items: [
+ { name: 'Production Checklist', url: '/guides/deployment/going-into-prod' },
+ {
+ name: 'Shared Responsibility Model',
+ url: '/guides/deployment/shared-responsibility-model',
+ },
+ { name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
+ { name: 'Hardening the Data API', url: '/guides/database/hardening-data-api' },
+ ],
+ },
],
}
diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.ComboBox.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.ComboBox.tsx
index 7b210ea3032..53d62179b15 100644
--- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.ComboBox.tsx
+++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.ComboBox.tsx
@@ -1,18 +1,18 @@
+import { noop } from 'lodash-es'
import { Check, ChevronsUpDown } from 'lucide-react'
-import { noop } from 'lodash'
import { useState } from 'react'
import {
Button_Shadcn_ as Button,
- Popover_Shadcn_ as Popover,
- PopoverTrigger_Shadcn_ as PopoverTrigger,
- PopoverContent_Shadcn_ as PopoverContent,
- Command_Shadcn_ as Command,
- CommandInput_Shadcn_ as CommandInput,
- CommandEmpty_Shadcn_ as CommandEmpty,
- CommandItem_Shadcn_ as CommandItem,
- CommandGroup_Shadcn_ as CommandGroup,
- CommandList_Shadcn_ as CommandList,
cn,
+ Command_Shadcn_ as Command,
+ CommandEmpty_Shadcn_ as CommandEmpty,
+ CommandGroup_Shadcn_ as CommandGroup,
+ CommandInput_Shadcn_ as CommandInput,
+ CommandItem_Shadcn_ as CommandItem,
+ CommandList_Shadcn_ as CommandList,
+ Popover_Shadcn_ as Popover,
+ PopoverContent_Shadcn_ as PopoverContent,
+ PopoverTrigger_Shadcn_ as PopoverTrigger,
ScrollArea,
} from 'ui'
diff --git a/apps/docs/components/SharedData.tsx b/apps/docs/components/SharedData.tsx
index f5b4cbc9111..113a8340195 100644
--- a/apps/docs/components/SharedData.tsx
+++ b/apps/docs/components/SharedData.tsx
@@ -1,4 +1,4 @@
-import { at } from 'lodash'
+import { at } from 'lodash-es'
import { ReactNode } from 'react'
import { config, logConstants } from 'shared-data'
diff --git a/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx b/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx
index d26c8eee48d..4c0af8c9c46 100644
--- a/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx
+++ b/apps/docs/content/guides/auth/auth-hooks/custom-access-token-hook.mdx
@@ -10,8 +10,9 @@ Claims returned must conform to our specification. Supabase Auth will check for
These are the fields currently available on an access token:
-Required Claims: `iss`, `aud`, `exp`, `iat`, `sub`, `role`, `aal`, `session_id`
-Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`, `email`, `phone`
+Required Claims: `iss`, `aud`, `exp`, `iat`, `sub`, `role`, `aal`, `session_id`, `email`, `phone`, `is_anonymous`
+
+Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`,
**Inputs**
@@ -241,7 +242,10 @@ as $$
'sub',
'role',
'aal',
- 'session_id'
+ 'session_id',
+ 'email',
+ 'phone',
+ 'is_anonymous'
] loop
if original_claims ? claim then
-- original_claims contains one of the listed claims, set it on new_claims
diff --git a/apps/docs/content/guides/database/hardening-data-api.mdx b/apps/docs/content/guides/database/hardening-data-api.mdx
index 64258cf784a..bfe1f421b7b 100644
--- a/apps/docs/content/guides/database/hardening-data-api.mdx
+++ b/apps/docs/content/guides/database/hardening-data-api.mdx
@@ -2,7 +2,24 @@
title: 'Hardening the Data API'
---
-Your database's automatically generated Data API exposes the `public` schema by default. If your `public` schema is used by other tools as a default space, you might want to lock down this schema. This helps prevent accidental exposure of data that's automatically added to `public`.
+Your database's auto-generated Data API exposes the `public` schema by default. You can change this to any schema in your database, or even disable the Data API completely.
+
+Any tables that are accessible through the Data API _must_ have [Row Level Security](/docs/guides/database/postgres/row-level-security) enabled. Row Level Security (RLS) is enabled by default when you create tables from the Supabase Dashboard. If you create a table using the SQL editor or your own SQL client or migration runner, you*must* enable RLS yourself.
+
+## Shared responsibility
+
+Your application's security is your responsibility as a developer. This includes RLS, falling under the [Shared Responsibility](/docs/guides/deployment/shared-responsibility-model) model. To help you:
+
+- Supabase sends daily emails warning of any tables that are exposed to the Data API which do not have RLS enabled.
+- Supabase provides a Security Advisor and other tools in the Supabase Dashboard to fix any issues.
+
+## Private schemas
+
+We highly recommend creating a `private` schema for storing tables that you do not want to expose via the Data API. These tables can be accessed via Supabase Edge Functions or any other serverside tool. In this model, you should implement your security model in your serverside code. Although it's not required, we _still_ recommend enabling RLS for private tables and then connecting to your database using a Postgres role with `bypassrls` privileges.
+
+## Managing the public schema
+
+If your `public` schema is used by other tools as a default space, you might want to lock down this schema. This helps prevent accidental exposure of data that's automatically added to `public`.
There are two levels of security hardening for the Data API:
diff --git a/apps/docs/content/guides/database/postgres/setup-replication-external.mdx b/apps/docs/content/guides/database/postgres/setup-replication-external.mdx
index 02bbbb939f3..0e74c9df844 100644
--- a/apps/docs/content/guides/database/postgres/setup-replication-external.mdx
+++ b/apps/docs/content/guides/database/postgres/setup-replication-external.mdx
@@ -27,9 +27,9 @@ select pg_create_logical_replication_slot('example_slot', 'pgoutput');
-This will need a direct connection to your database and you can find the connection info in the [Dashboard](https://supabase.com/dashboard/project/_/settings/database).
+This will need a **direct** connection (not a Connection Pooler) to your database and you can find the connection info in the [Dashboard](https://supabase.com/dashboard/project/_/settings/database).
-You will also need to ensure that ipv6 is supported by your replication destination.
+You will also need to ensure that IPv6 is supported by your replication destination (or you can enable the [IPv4 add-on](/guides/platform/ipv4-address))
If you would prefer not to use the `postgres` user, then you can run `CREATE ROLE WITH REPLICATION;` using the `postgres` user.
diff --git a/apps/docs/content/guides/database/replication.mdx b/apps/docs/content/guides/database/replication.mdx
index 18f2d05a604..ee5445662ff 100644
--- a/apps/docs/content/guides/database/replication.mdx
+++ b/apps/docs/content/guides/database/replication.mdx
@@ -1,93 +1,69 @@
---
-id: 'replication'
-title: 'Replication'
-slug: 'replication'
+title: 'Replication and change data capture'
+description: 'An introduction to logical replication and change data capture'
---
-Replication is a technique for copying the data from one database to another. Supabase uses replication functionality to provide a real-time API. Replication is useful for:
+Replication is the process of copying changes from your database to another location. It's also referred to as change data capture (CDC): capturing all the changes that occur to your data.
-- Spreading out the "load." For example, if your database has a lot of reads, you might want to split it between two databases.
-- Reducing latency. For example, you may want one database in London to serve your European customers, and one in New York to serve the US.
+## Use cases
-Replication is done through _publications_, a method of choosing which changes to send to other systems (usually another Postgres database). Publications can be managed in the [Dashboard](https://supabase.com/dashboard) or with SQL.
+You might use replication for:
-## Manage publications in the dashboard
+- **Analytics and Data Warehousing**: Replicate your operational database to analytics platforms like BigQuery for complex analysis without impacting your application's performance.
+- **Data Integration**: Keep your data synchronized across different systems and services in your tech stack.
+- **Backup and Disaster Recovery**: Maintain up-to-date copies of your data in different locations.
+- **Read Scaling**: Distribute read operations across multiple database instances to improve performance.
-1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
-2. Click on **Publications** in the sidebar.
-3. Control which database events are sent by toggling **Insert**, **Update**, and **Delete**.
-4. Control which tables broadcast changes by selecting **Source** and toggling each table.
+## Replication in Postgres
-
+Postgres comes with built-in support for replication via publications and replication slots. Refer to the [Concepts and terms](#concepts-and-terms) section to learn how replication works.
-## Create a publication
+## Setting up and monitoring replication in Supabase
-This publication contains changes to all tables.
+- [Setting up replication](/docs/guides/database/replication/setting-up-replication)
+- [Monitoring replication](/docs/guides/database/replication/monitoring-replication)
-```sql
-create publication publication_name
-for all tables;
-```
+
-## Create a publication to listen to individual tables
+If you want to set up a read replica, see [Read Replicas](/docs/guides/platform/read-replicas) instead. If you want to sync your data in real time to a client such as a browser or mobile app, see [Realtime](/docs/guides/realtime) instead.
-```sql
-create publication publication_name
-for table table_one, table_two;
-```
+
-## Add tables to an existing publication
+## Concepts and terms
-```sql
-alter publication publication_name
-add table table_name;
-```
+### Write-Ahead Log (WAL)
-## Listen to `insert`
+Postgres uses a system called the Write-Ahead Log (WAL) to manage changes to the database. As you make changes, they are appended to the WAL (which is a series of files (also called "segments"), where the file size can be specified). Once one segment is full, Postgres will start appending to a new segment. After a period of time, a checkpoint occurs and Postgres synchronizes the WAL with your database. Once the checkpoint is complete, then the WAL files can be removed from disk and free up space.
-```sql
-create publication publication_name
-for all tables
-with (publish = 'insert');
-```
+### Logical replication and WAL
-## Listen to `update`
+Logical replication is a method of replication where Postgres uses the WAL files and transmit those changes to another Postgres database, or a system that supports reading WAL files.
-```sql
-create publication publication_name
-for all tables
-with (publish = 'update');
-```
+### LSN
-## Listen to `delete`
+LSN is a Log Sequence Number that is used to identify the position of a WAL file in the WAL directory. It is often used to determine the progress of replication in subscribers and calculate the lag of a replication slot.
-```sql
-create publication publication_name
-for all tables
-with (publish = 'delete');
-```
+## Logical replication architecture
-## Remove a publication
+When setting up logical replication, three key components are involved:
-```sql
-drop publication if exists publication_name;
-```
+- `publication` - A set of tables on your primary database that will be `published`
+- `replication slot` - A slot used for replicating the data from a single publication. The slot, when created, will specify the output format of the changes
+- `subscription` - A subscription is created from an external system (i.e. another Postgres database) and must specify the name of the `publication`. If you do not specify a replication slot, one is automatically created
-## Recreate a publication
+## Logical replication output format
-If you're recreating a publication, it's best to do it in a transaction to ensure the operation succeeds.
+Logical replication is typically output in 2 forms, `pgoutput` and `wal2json`. The output method is how Postgres sends changes to any active replication slot.
-```sql
-begin;
- -- remove the realtime publication
- drop publication if exists publication_name;
+## Logical replication configuration
- -- re-create the publication but don't enable it for any tables
- create publication publication_name;
-commit;
-```
+When using logical replication, Postgres is then configured to keep WAL files around for longer than it needs them. If the files are removed too quickly, then your `replication slot` will become inactive and, if the database receives a large number of changes in a short time, then the `replication slot` can become lost as it was not able to keep up.
+
+In order to mitigate this, Postgres has many options and settings that can be [tweaked](/guides/database/custom-postgres-config) to manage the WAL usage effectively. Not all of these settings are user configurable as they can impact the stability of your database. For those that are, these should be considered as advanced configuration and not changed without understanding that they can cause additional disk space and resources to be used, as well as incur additional costs.
+
+| Setting | Description | User-facing | Default |
+| ---------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------- | ------- |
+| [`max_replication_slots`](https://postgresqlco.nf/doc/en/param/max_replication_slots/) | Max count of replication slots allowed | No | |
+| [`wal_keep_size`](https://postgresqlco.nf/doc/en/param/wal_keep_size/) | Minimum size of WAL files to keep for replication | No | |
+| [`max_slot_wal_keep_size`](https://postgresqlco.nf/doc/en/param/max_slot_wal_keep_size/) | Max WAL size that can be reserved by replication slots | No | |
+| [`checkpoint_timeout`](https://postgresqlco.nf/doc/en/param/checkpoint_timeout/) | Max time between WAL checkpoints | No | |
diff --git a/apps/docs/content/guides/database/replication/faq.mdx b/apps/docs/content/guides/database/replication/faq.mdx
new file mode 100644
index 00000000000..f0bdd97f465
--- /dev/null
+++ b/apps/docs/content/guides/database/replication/faq.mdx
@@ -0,0 +1,41 @@
+---
+id: 'replication-faq'
+title: 'FAQs'
+description: 'Considerations and FAQs when setting up replication'
+sidebar_label: 'FAQs'
+---
+
+# Which connection string should be used?
+
+Always use the direct connection string for logical replication.
+
+Connections through a pooler, such as Supavisor, will not work.
+
+# The tool in use does not support IPv6
+
+You can enable the [IPv4 add-on](/guides/platform/ipv4-address) for your project.
+
+# What is XMIN and should it be used?
+
+Xmin is a different form of replication from logical replication and should only be used if logical replication is not available for your database (i.e. older versions of Postgres).
+
+Xmin performs replication by checking the [xmin system column](https://www.postgresql.org/docs/current/ddl-system-columns.html) and determining if that row has already been synchronized.
+
+It does not capture deletion of data and is **not recommended**, particularly for larger databases.
+
+# Can replication be configured in the Dashboard?
+
+You can view [publications](https://supabase.com/dashboard/project/default/database/publications) in the Dashboard but all steps to configure replication must be done using the [SQL Editor](https://supabase.com/dashboard/project/default/sql/new) or a CLI tool of your choice.
+
+# How to configure database settings for replication?
+
+Yes. Using the Supabase CLI, you can [configure database settings](/guides/database/custom-postgres-config#cli-configurable-settings) to optimize them for your replication needs. These values can vary depending on the activity of your database size and activity.
+
+# What are some important configuration options?
+
+Some of the more important options to be aware of are:
+
+- `max_wal_size`
+- `max_slot_wal_keep_size`
+- `wal_keep_size`
+- `max_wal_senders`
diff --git a/apps/docs/content/guides/database/replication/monitoring-replication.mdx b/apps/docs/content/guides/database/replication/monitoring-replication.mdx
new file mode 100644
index 00000000000..a49b8a8f27a
--- /dev/null
+++ b/apps/docs/content/guides/database/replication/monitoring-replication.mdx
@@ -0,0 +1,94 @@
+---
+title: 'Monitoring replication'
+---
+
+Monitoring replication lag is important and there are 3 ways to do this:
+
+1. Dashboard - Under the [Reports](/guides/platform/reports) of the dashboard, you can view the replication lag of your project
+2. Database -
+ - pg_stat_subscription (subscriber) - if PID is null, then the subscription is not active
+ - pg_stat_subscription_stats - look here for error_count to see if there were issues applying or syncing (if yes, check the logs for why)
+ - pg_replication_slots - use this to check if the slot is active and you can also calculate the lag from here
+3. [Metrics](/guides/telemetry/metrics) - Using the prometheus endpoint for your project
+ - replication_slots_max_lag_bytes - this is the more important one
+ - pg_stat_replication_replay_lag - lag to replay WAL files from the source DB on the target DB (throttled by disk or high activity)
+ - pg_stat_replication_send_lag - lag in sending WAL files from the source DB (a high lag means that the publisher is not being asked to send new WAL files OR a network issues)
+
+## Primary
+
+### Replication status and lag
+
+The `pg_stat_replication` table shows the status of any replicas connected to the primary database.
+
+```sql
+select pid, application_name, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, sync_state
+from pg_stat_replication;
+```
+
+### Replication slot status
+
+A replication slot can be in one of three states:
+
+- `active` - The slot is active and is receiving data
+- `inactive` - The slot is not active and is not receiving data
+- `lost` - The slot is lost and is not receiving data
+
+The state can be checked using the `pg_replication_slots` table:
+
+```sql
+select slot_name, active, state from pg_replication_slots;
+```
+
+### WAL size
+
+The WAL size can be checked using the `pg_ls_waldir()` function:
+
+```sql
+select * from pg_ls_waldir();
+```
+
+### Check LSN
+
+```sql
+select pg_current_wal_lsn();
+```
+
+## Subscriber
+
+### Subscription status
+
+The `pg_subscription` table shows the status of any subscriptions on a replica and the `pg_subscription_rel` table shows the status of each table within a subscription.
+
+The `srsubstate` column in `pg_subscription_rel` can be one of the following:
+
+- `i` - Initializing - The subscription is being initialized
+- `d` - Data Synchronizing - The subscription is synchronizing data for the first time (i.e. doing the initial copy)
+- `s` - Synchronized - The subscription is synchronized
+- `r` - Replicating - The subscription is replicating data
+
+```sql
+SELECT
+ sub.subname AS subscription_name,
+ relid::regclass AS table_name,
+ srel.srsubstate AS replication_state,
+ CASE srel.srsubstate
+ WHEN 'i' THEN 'Initializing'
+ WHEN 'd' THEN 'Data Synchronizing'
+ WHEN 's' THEN 'Synchronized'
+ WHEN 'r' THEN 'Replicating'
+ ELSE 'Unknown'
+ END AS state_description,
+ srel.srsyncedlsn AS last_synced_lsn
+FROM
+ pg_subscription sub
+JOIN
+ pg_subscription_rel srel ON sub.oid = srel.srsubid
+ORDER BY
+ table_name;
+```
+
+### Check LSN
+
+```sql
+select pg_last_wal_replay_lsn();
+```
diff --git a/apps/docs/content/guides/database/replication/setting-up-replication.mdx b/apps/docs/content/guides/database/replication/setting-up-replication.mdx
new file mode 100644
index 00000000000..170b54b64d4
--- /dev/null
+++ b/apps/docs/content/guides/database/replication/setting-up-replication.mdx
@@ -0,0 +1,86 @@
+---
+id: 'setting-up-replication'
+title: 'Setting up replication and CDC with Supabase'
+description: 'Performing Extract Transform Load (ETL) with Supabase'
+sidebar_label: 'Setting up replication and CDC'
+---
+
+## Prerequisites
+
+To set up replication, the following is recommended:
+
+- Instance size of XL or greater
+- [IPv4 add-on](/guides/platform/ipv4-address) enabled
+
+To create a replication slot, you will need to use the `postgres` user and follow the instructions in our [guide](/guides/database/postgres/setup-replication-external).
+
+
+ If you are running Postgres 17 or higher, you can create a new user and grant them replication
+ permissions with the `postgres` user. For versions below 17, you will need to use the `postgres`
+ user.
+
+
+If you are replicating to an external system and using any of the tools below, check their documentation first and we have added additional information where the setup with Supabase can vary.
+
+
+
+
+
+Airbyte has the following [documentation](https://docs.airbyte.com/integrations/sources/postgres/) for setting up Postgres as a source, either in their cloud offering or by self-hosting.
+
+You can follow those steps with the following modifications:
+
+1. Use the `postgres` user
+2. Select `logical replication` as the replication method (`xmin` is possible, but not recommended)
+
+## Troubleshooting
+
+Airbyte has a known [issue](https://discuss.airbyte.io/t/postgres-source-replication-slot-safe-wal-size-only-reset-when-a-change-occurs/3263/7) where it does not clear WAL files on each successful sync. The recommended workaround is to have a `heartbeat` table that you write changes to once an hour.>
+
+
+
+
+Estuary has the following [documentation](https://docs.estuary.dev/reference/Connectors/capture-connectors/PostgreSQL/Supabase/) for setting up Postgres as a source.
+
+
+
+
+
+Fivetran has the following [documentation](https://fivetran.com/docs/connectors/databases/postgresql/setup-guide) for setting up Postgres as a source.
+
+You can follow those steps with the following modifications:
+
+1. In Step 2, choose `logical replication` as the sync mechanism
+2. In Step 3, do not create a user and use the existing `postgres` user for replication
+3. In Step 5, no need to modify any WAL settings as we have done that for you
+
+
+
+
+
+Materialize has the following [documentation](https://materialize.com/docs/sql/create-source/postgres/) on setting up Postgres as a source.
+
+You can follow those steps with the following modifications:
+
+1. Follow the steps in our [guide](/guides/database/postgres/setup-replication-external) to create a publication slot
+
+
+
+
+
+Stitch has the following [documentation](https://www.stitchdata.com/docs/integrations/databases/postgresql/v2#extract-data) on configuring Postgres as a source.
+
+You can follow those steps with the following modifications:
+
+1. Use the `postgres` user for replication
+2. Skip step 3
+
+
+
+
diff --git a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx
index 74d6bdff874..9f979e821ee 100644
--- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx
+++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx
@@ -15,7 +15,7 @@ Data sent to the client when retrieving data stored in your database.
-There are various ways to interact with your database, such as through the PostgREST API using one of the client SDKs or via the Supavisor connection pooler. On the Supabase Dashboard, Egress from the PostgREST API is labeled as **Database Egress**, while Egress through Supavisor is labeled as **Supavisor Egress**.
+There are various ways to interact with your database, such as through the PostgREST API using one of the client SDKs or via the Supavisor connection pooler. On the Supabase Dashboard, Egress from the PostgREST API is labeled as **Database Egress**, while Egress through Supavisor is labeled as **Shared Pooler Egress**.
@@ -43,6 +43,12 @@ Data pushed to clients via Supabase Realtime for subscribed events.
**Example:** When a user views a product page in an online shop, their client subscribes to real-time inventory updates. As stock levels change, Supabase Realtime pushes updates to all subscribed clients, contributing to Realtime Egress.
+### Shared pooler Egress
+
+Data sent to the client when using the shared connection pooler (Supavisor) to access your database. WHen using the shared connection pooler, we do not count database egress, as this would otherwise count double (Database -> Shared Pooler + Shared Pooler -> Client).
+
+**Example:** You are using our [shared connection pooler](https://supabase.com/docs/guides/database/connecting-to-postgres#shared-pooler) and you query a list of invoices in your backend. The data returned from that query is contributing to Shared Pooler Egress.
+
### Log Drain Egress
Data pushed to the connected log drain.
diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx
index abc206aa9ce..14fc142a370 100644
--- a/apps/docs/content/guides/realtime/broadcast.mdx
+++ b/apps/docs/content/guides/realtime/broadcast.mdx
@@ -76,11 +76,15 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj
```python
- from supabase import create_client
+ import asyncio
+ from supabase import acreate_client
URL = "https://.supabase.co"
KEY = ""
- supabase = create_client(URL, KEY)
+
+ async def create_supabase():
+ supabase = await acreate_client(URL, KEY)
+ return supabase
```
@@ -179,6 +183,11 @@ You can provide a callback for the `broadcast` channel to receive message. This
+
+
+ In the following Realtime examples, certain methods are awaited. These should be enclosed within an `async` function.
+
+
{/* prettier-ignore */}
```python
@@ -190,7 +199,7 @@ You can provide a callback for the `broadcast` channel to receive message. This
print(f"Broadcast received: {payload}")
# Subscribe to the Channel
- my_channel
+ await my_channel
.on_broadcast('shout', message_received) # Listen for "shout". Can be "*" to listen to all events
.subscribe()
```
@@ -323,6 +332,12 @@ You can use the Supabase client libraries to send Broadcast messages.
+
+
+ When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`.
+
+
+
{/* prettier-ignore */}
```python
my_channel = supabase.channel('test-channel')
@@ -332,12 +347,12 @@ You can use the Supabase client libraries to send Broadcast messages.
if status != RealtimeSubscribeStates.SUBSCRIBED:
return
- my_channel.send_broadcast(
+ asyncio.create_task(my_channel.send_broadcast(
'shout',
{ "message": 'hello, world' },
- )
+ ))
- my_channel.subscribe(on_subscribe)
+ await my_channel.subscribe(on_subscribe)
```
@@ -543,6 +558,12 @@ You can pass configuration options while initializing the Supabase Client.
+
+
+ When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`.
+
+
+
By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `True`.
```python
@@ -559,10 +580,10 @@ You can pass configuration options while initializing the Supabase Client.
return
# Send a message once the client is subscribed
- channel_b.send_broadcast(
+ asyncio.create_task(channel_b.send_broadcast(
'test-my-messages',
{ "message": 'talking to myself' },
- )
+ ))
my_channel.subscribe(on_subscribe)
```
diff --git a/apps/docs/features/docs/Reference.generated.script.ts b/apps/docs/features/docs/Reference.generated.script.ts
index 42c5f1b6a5c..b4cefeec563 100644
--- a/apps/docs/features/docs/Reference.generated.script.ts
+++ b/apps/docs/features/docs/Reference.generated.script.ts
@@ -1,4 +1,4 @@
-import { isPlainObject, keyBy } from 'lodash'
+import { isPlainObject, keyBy } from 'lodash-es'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
diff --git a/apps/docs/features/docs/Reference.navigation.client.tsx b/apps/docs/features/docs/Reference.navigation.client.tsx
index 4ef50c32ff9..ca574e3e4c0 100644
--- a/apps/docs/features/docs/Reference.navigation.client.tsx
+++ b/apps/docs/features/docs/Reference.navigation.client.tsx
@@ -2,7 +2,7 @@
import * as Collapsible from '@radix-ui/react-collapsible'
-import { debounce } from 'lodash'
+import { debounce } from 'lodash-es'
import { ChevronUp } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
diff --git a/apps/docs/features/docs/Reference.ui.tsx b/apps/docs/features/docs/Reference.ui.tsx
index 4cc062daf17..a1047dbf4df 100644
--- a/apps/docs/features/docs/Reference.ui.tsx
+++ b/apps/docs/features/docs/Reference.ui.tsx
@@ -1,4 +1,4 @@
-import { isEqual } from 'lodash'
+import { isEqual } from 'lodash-es'
import { ChevronRight, XCircle } from 'lucide-react'
import type { HTMLAttributes, PropsWithChildren } from 'react'
import ReactMarkdown from 'react-markdown'
diff --git a/apps/docs/features/docs/Reference.utils.ts b/apps/docs/features/docs/Reference.utils.ts
index b95db420a45..9687a001ff8 100644
--- a/apps/docs/features/docs/Reference.utils.ts
+++ b/apps/docs/features/docs/Reference.utils.ts
@@ -1,6 +1,6 @@
import { fromMarkdown } from 'mdast-util-from-markdown'
-import { toMarkdown } from 'mdast-util-to-markdown'
import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx'
+import { toMarkdown } from 'mdast-util-to-markdown'
import { mdxjs } from 'micromark-extension-mdxjs'
import type { Metadata, ResolvingMetadata } from 'next'
import { redirect } from 'next/navigation'
@@ -131,7 +131,7 @@ export async function generateReferenceMetadata(
const isApiReference = parsedPath.__type === 'api'
const isSelfHostingReference = parsedPath.__type === 'self-hosting'
if (isClientSdkReference) {
- const { sdkId, maybeVersion } = parsedPath
+ const { sdkId, maybeVersion, path } = parsedPath
const version = maybeVersion ?? REFERENCES[sdkId].versions[0]
const flattenedSections = await getFlattenedSections(sdkId, version)
@@ -141,7 +141,7 @@ export async function generateReferenceMetadata(
slug.length > 0
? flattenedSections.find((section) => section.slug === slug[0])?.title
: undefined
- const url = [BASE_PATH, 'reference', sdkId, maybeVersion, slug[0]].filter(Boolean).join('/')
+ const url = [BASE_PATH, 'reference', sdkId, path[0]].filter(Boolean).join('/')
const images = generateOpenGraphImageMeta({
type: 'API Reference',
diff --git a/apps/docs/features/docs/Troubleshooting.page.tsx b/apps/docs/features/docs/Troubleshooting.page.tsx
index e57e1b872b0..ff6b238c202 100644
--- a/apps/docs/features/docs/Troubleshooting.page.tsx
+++ b/apps/docs/features/docs/Troubleshooting.page.tsx
@@ -1,11 +1,11 @@
+import { Github } from 'lucide-react'
+import Link from 'next/link'
import { cn } from 'ui'
-
import Breadcrumbs from '~/components/Breadcrumbs'
import { Feedback } from '~/components/Feedback'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
import { MDXRemoteBase } from './MdxBase'
import { getTroubleshootingUpdatedDates, type ITroubleshootingEntry } from './Troubleshooting.utils'
-import Link from 'next/link'
import { formatError, serializeTroubleshootingSearchParams } from './Troubleshooting.utils.shared'
export default async function TroubleshootingPage({ entry }: { entry: ITroubleshootingEntry }) {
@@ -95,6 +95,19 @@ export default async function TroubleshootingPage({ entry }: { entry: ITroublesh
>
)}
+ {entry.data.github_url && (
+ <>
+
+
+
+ View discussion on GitHub
+
+ >
+ )}
diff --git a/apps/docs/features/docs/Troubleshooting.script.mjs b/apps/docs/features/docs/Troubleshooting.script.mjs
index b4f03549c3c..e3a710b8747 100644
--- a/apps/docs/features/docs/Troubleshooting.script.mjs
+++ b/apps/docs/features/docs/Troubleshooting.script.mjs
@@ -7,6 +7,8 @@
* Discussions.
*/
+import _configureDotEnv from '../../scripts/utils/dotenv'
+
import { createAppAuth } from '@octokit/auth-app'
import { Octokit } from '@octokit/core'
import { createClient } from '@supabase/supabase-js'
@@ -20,13 +22,12 @@ import { gfm } from 'micromark-extension-gfm'
import { mdxjs } from 'micromark-extension-mdxjs'
import { readFile, writeFile } from 'node:fs/promises'
import { parse, stringify } from 'smol-toml'
-
import {
getAllTroubleshootingEntriesInternal as getAllTroubleshootingEntries,
getArticleSlug,
} from './Troubleshooting.utils.common.mjs'
-import 'dotenv/config'
+const _ = _configureDotEnv
const REPOSITORY_ID = 'MDEwOlJlcG9zaXRvcnkyMTQ1ODcxOTM='
const TROUBLESHOOTING_CATEGORY_ID = 'DIC_kwDODMpXOc4CUvEr'
@@ -52,9 +53,9 @@ function octokit() {
octokitInstance = new Octokit({
authStrategy: createAppAuth,
auth: {
- appId: process.env.SEARCH_GITHUB_APP_ID,
- installationId: process.env.SEARCH_GITHUB_APP_INSTALLATION_ID,
- privateKey: process.env.SEARCH_GITHUB_APP_PRIVATE_KEY,
+ appId: process.env.DOCS_GITHUB_APP_ID,
+ installationId: process.env.DOCS_GITHUB_APP_INSTALLATION_ID,
+ privateKey: process.env.DOCS_GITHUB_APP_PRIVATE_KEY,
},
})
}
diff --git a/apps/docs/generator/helpers.ts b/apps/docs/generator/helpers.ts
index 30b8037d818..b0ab9705c03 100644
--- a/apps/docs/generator/helpers.ts
+++ b/apps/docs/generator/helpers.ts
@@ -1,5 +1,5 @@
import * as fs from 'fs'
-import { mapValues, values } from 'lodash'
+import { mapValues, values } from 'lodash-es'
export const slugify = (text: string) => {
if (!text) return ''
diff --git a/apps/docs/generator/legacy.ts b/apps/docs/generator/legacy.ts
index cf066ef1d0a..bc248b769a3 100644
--- a/apps/docs/generator/legacy.ts
+++ b/apps/docs/generator/legacy.ts
@@ -8,15 +8,15 @@
* tsx ReferenceGenerator.ts -o docs/client spec/supabase.yml
*/
+import * as fs from 'fs'
+import * as yaml from 'js-yaml'
+import { uniqBy } from 'lodash-es'
+import { flattenSections } from '../lib/helpers'
import Example from './legacy/components/Example'
import Page from './legacy/components/Page'
import Tab from './legacy/components/Tab'
+import { OpenRef, TsDoc } from './legacy/definitions'
import { slugify, tsDocCommentToMdComment, writeToDisk } from './legacy/lib/helpers'
-import { TsDoc, OpenRef } from './legacy/definitions'
-import { uniqBy } from 'lodash'
-import * as fs from 'fs'
-import * as yaml from 'js-yaml'
-import { flattenSections } from '../lib/helpers'
const commonDocSpecJson = JSON.parse(
fs.readFileSync('spec/common-client-libs-sections.json', 'utf8')
diff --git a/apps/docs/instrumentation-client.ts b/apps/docs/instrumentation-client.ts
index 86fc0cd1a4b..7d9f53573e2 100644
--- a/apps/docs/instrumentation-client.ts
+++ b/apps/docs/instrumentation-client.ts
@@ -3,6 +3,7 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs'
+import { hasConsented, IS_PLATFORM } from 'common'
import { IS_DEV } from './lib/constants'
if (!IS_DEV) {
@@ -20,13 +21,12 @@ if (!IS_DEV) {
],
beforeSend(event) {
- const frames = event.exception?.values?.[0].stacktrace?.frames || []
+ if (!IS_PLATFORM || !hasConsented()) {
+ return null
+ }
- // Drop errors not originating from docs app static files as unactionable
- if (
- frames &&
- !frames.some((frame) => frame.filename && frame.filename.startsWith('app:///_next'))
- ) {
+ const frames = event.exception?.values?.[0].stacktrace?.frames || []
+ if (isThirdPartyError(frames)) {
return null
}
@@ -35,4 +35,24 @@ if (!IS_DEV) {
})
}
+// We want to ignore errors not originating from docs app static files
+// (such as errors from browser extensions). Those errors come from files
+// not starting with 'app:///_next'.
+//
+// However, there is a complication because the Sentry code that sends
+// the error shows up in the stack trace, and that _does_ start with
+// 'app:///_next'. It is always the first frame in the stack trace,
+// and has a specific pre_context comment that we can use for filtering.
+function isThirdPartyError(frames: Sentry.StackFrame[] | undefined) {
+ if (!frames) return false
+
+ function isSentryFrame(frame: Sentry.StackFrame, index: number) {
+ return index === 0 && frame.pre_context?.[0]?.includes('sentry.javascript')
+ }
+
+ return !frames.some((frame, index) => {
+ frame.abs_path?.startsWith('app:///_next') && !isSentryFrame(frame, index)
+ })
+}
+
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart
diff --git a/apps/docs/lib/refGenerator/helpers.ts b/apps/docs/lib/refGenerator/helpers.ts
index f377d56155c..bd0de078ead 100644
--- a/apps/docs/lib/refGenerator/helpers.ts
+++ b/apps/docs/lib/refGenerator/helpers.ts
@@ -1,9 +1,9 @@
import type { TsDoc } from '../../generator/legacy/definitions'
-import { values, mapValues } from 'lodash'
+import { mapValues, values } from 'lodash-es'
import type { OpenAPIV3 } from 'openapi-types'
-import { flattenSections } from '../helpers'
import type { ICommonItem } from '~/components/reference/Reference.types'
+import { flattenSections } from '../helpers'
export function extractTsDocNode(nodeToFind: string, definition: any) {
const nodePath = nodeToFind.split('.')
diff --git a/apps/docs/lib/remotePatterns.js b/apps/docs/lib/remotePatterns.js
index 2b06dc1c058..e30aa6d1294 100644
--- a/apps/docs/lib/remotePatterns.js
+++ b/apps/docs/lib/remotePatterns.js
@@ -1,4 +1,4 @@
-module.exports = [
+export default [
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
diff --git a/apps/docs/lib/supabase.ts b/apps/docs/lib/supabase.ts
index 3d3ab8820f8..7c46ef9c993 100644
--- a/apps/docs/lib/supabase.ts
+++ b/apps/docs/lib/supabase.ts
@@ -2,6 +2,7 @@ import { createClient, type SupabaseClient } from '@supabase/supabase-js'
import { type Database as DatabaseGenerated } from 'common'
type Database = {
+ content: DatabaseGenerated['content']
graphql_public: DatabaseGenerated['graphql_public']
public: {
Tables: DatabaseGenerated['public']['Tables']
diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs
index 400ac7c4b3f..cbea04fe398 100644
--- a/apps/docs/next.config.mjs
+++ b/apps/docs/next.config.mjs
@@ -34,13 +34,6 @@ const nextConfig = {
// @ts-ignore
remotePatterns,
},
- // TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported.
- // mdxRs: true,
- modularizeImports: {
- lodash: {
- transform: 'lodash/{{member}}',
- },
- },
webpack: (config) => {
config.module.rules.push({
test: /\.include$/,
diff --git a/apps/docs/package.json b/apps/docs/package.json
index b98508a1c73..be764959be4 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -2,37 +2,38 @@
"name": "docs",
"version": "0.0.0",
"private": true,
+ "type": "module",
"scripts": {
- "preinstall": "npx only-allow pnpm",
- "predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
- "dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"",
- "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs",
- "dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs",
- "prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
"build": "next build",
"build:analyze": "ANALYZE=true next build",
- "build:sitemap": "tsx ./internals/generate-sitemap.ts",
"build:llms": "tsx ./scripts/llms.ts",
- "postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
- "start": "next start",
+ "build:sitemap": "tsx ./internals/generate-sitemap.ts",
+ "clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__",
+ "codegen:examples": "cp -r ../../examples ./examples",
+ "codegen:graphql": "tsx --conditions=react-server ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts",
+ "codegen:references": "tsx features/docs/Reference.generated.script.ts",
+ "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"",
+ "dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"",
+ "dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs",
+ "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs",
+ "embeddings": "tsx scripts/search/generate-embeddings.ts",
+ "embeddings:refresh": "pnpm run embeddings --refresh",
+ "last-changed": "tsx scripts/last-changed.ts",
+ "last-changed:reset": "pnpm run last-changed -- --reset",
"lint": "next lint",
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
- "pretypecheck": "pnpm run codegen:graphql",
- "typecheck": "tsc --noEmit",
+ "postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
+ "prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
+ "predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
+ "preinstall": "npx only-allow pnpm",
"pretest": "pnpm run codegen:examples",
+ "pretypecheck": "pnpm run codegen:graphql",
+ "start": "next start",
"test": "pnpm supabase start && pnpm run test:local && pnpm supabase stop",
"test:local": "vitest --exclude \"**/*.smoke.test.ts\"",
"test:smoke": "pnpm run codegen:references && vitest -t \"prod smoke test\"",
- "embeddings": "tsx scripts/search/generate-embeddings.ts",
- "embeddings:refresh": "pnpm run embeddings --refresh",
"troubleshooting:sync": "node features/docs/Troubleshooting.script.mjs",
- "last-changed": "tsx scripts/last-changed.ts",
- "last-changed:reset": "pnpm run last-changed -- --reset",
- "codegen:examples": "cp -r ../../examples ./examples",
- "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"",
- "codegen:graphql": "tsx --conditions=react-server ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts",
- "codegen:references": "tsx features/docs/Reference.generated.script.ts",
- "clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__"
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
"@har-sdk/openapi-sampler": "^2.2.0",
@@ -70,7 +71,7 @@
"jsrsasign": "^11.0.0",
"katex": "^0.16.21",
"libpg-query": "15.2.0",
- "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
"lucide-react": "*",
"mdast": "^3.0.0",
"mdast-util-from-markdown": "^1.2.0",
@@ -125,6 +126,7 @@
"@types/graphql-validation-complexity": "^0.4.4",
"@types/hast": "^2.3.4",
"@types/jsrsasign": "^10.5.12",
+ "@types/lodash-es": "^4.17.12",
"@types/mdast": "^3.0.0",
"@types/node": "catalog:",
"@types/react": "catalog:",
diff --git a/apps/docs/postcss.config.js b/apps/docs/postcss.config.cjs
similarity index 100%
rename from apps/docs/postcss.config.js
rename to apps/docs/postcss.config.cjs
diff --git a/apps/docs/resources/error/errorModel.ts b/apps/docs/resources/error/errorModel.ts
new file mode 100644
index 00000000000..ce9cb1d7c60
--- /dev/null
+++ b/apps/docs/resources/error/errorModel.ts
@@ -0,0 +1,74 @@
+import { ApiErrorGeneric, convertPostgrestToApiError, NoDataError } from '~/app/api/utils'
+import { Result } from '~/features/helpers.fn'
+import { supabase } from '~/lib/supabase'
+
+export const SERVICES = {
+ AUTH: {
+ value: 'AUTH',
+ },
+ REALTIME: {
+ value: 'REALTIME',
+ },
+ STORAGE: {
+ value: 'STORAGE',
+ },
+} as const
+
+type Service = keyof typeof SERVICES
+
+export class ErrorModel {
+ public code: string
+ public service: Service
+ public httpStatusCode?: number
+ public message?: string
+
+ constructor({
+ code,
+ service,
+ httpStatusCode: httpStatusCode,
+ message,
+ }: {
+ code: string
+ service: Service
+ httpStatusCode?: number
+ message?: string
+ }) {
+ this.code = code
+ this.service = service
+ this.httpStatusCode = httpStatusCode
+ this.message = message
+ }
+
+ static async loadSingleError({
+ code,
+ service,
+ }: {
+ code: string
+ service: Service
+ }): Promise> {
+ return new Result(
+ await supabase()
+ .schema('content')
+ .from('error')
+ .select('code, ...service(service:name), httpStatusCode:http_status_code, message')
+ .eq('code', code)
+ .eq('service.name', service)
+ .is('deleted_at', null)
+ .single<{
+ code: string
+ service: Service
+ httpStatusCode?: number
+ message?: string
+ }>()
+ )
+ .map((data) => {
+ return new ErrorModel(data)
+ })
+ .mapError((error) => {
+ if (error.code === 'PGRST116') {
+ return new NoDataError('Error for given code and service does not exist', error)
+ }
+ return convertPostgrestToApiError(error)
+ })
+ }
+}
diff --git a/apps/docs/resources/error/errorResolver.ts b/apps/docs/resources/error/errorResolver.ts
new file mode 100644
index 00000000000..ef79e2d887b
--- /dev/null
+++ b/apps/docs/resources/error/errorResolver.ts
@@ -0,0 +1,43 @@
+import { GraphQLError, GraphQLNonNull, GraphQLResolveInfo, GraphQLString } from 'graphql'
+import { type RootQueryTypeErrorArgs } from '~/__generated__/graphql'
+import { convertUnknownToApiError } from '~/app/api/utils'
+import { Result } from '~/features/helpers.fn'
+import { ErrorModel } from './errorModel'
+import {
+ GRAPHQL_FIELD_ERROR_GLOBAL,
+ GraphQLEnumTypeService,
+ GraphQLObjectTypeError,
+} from './errorSchema'
+
+async function resolveSingleError(
+ _parent: unknown,
+ args: RootQueryTypeErrorArgs,
+ _context: unknown,
+ _info: GraphQLResolveInfo
+): Promise {
+ return (
+ await Result.tryCatchFlat(ErrorModel.loadSingleError, convertUnknownToApiError, args)
+ ).match(
+ (data) => data,
+ (error) => {
+ console.error(`Error resolving ${GRAPHQL_FIELD_ERROR_GLOBAL}:`, error)
+ return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
+ }
+ )
+}
+
+export const errorRoot = {
+ [GRAPHQL_FIELD_ERROR_GLOBAL]: {
+ description: 'Get the details of an error code returned from a Supabase service',
+ args: {
+ code: {
+ type: new GraphQLNonNull(GraphQLString),
+ },
+ service: {
+ type: new GraphQLNonNull(GraphQLEnumTypeService),
+ },
+ },
+ type: GraphQLObjectTypeError,
+ resolve: resolveSingleError,
+ },
+}
diff --git a/apps/docs/resources/error/errorSchema.ts b/apps/docs/resources/error/errorSchema.ts
new file mode 100644
index 00000000000..c752d705336
--- /dev/null
+++ b/apps/docs/resources/error/errorSchema.ts
@@ -0,0 +1,40 @@
+import {
+ GraphQLEnumType,
+ GraphQLInt,
+ GraphQLNonNull,
+ GraphQLObjectType,
+ GraphQLString,
+} from 'graphql'
+import { SERVICES } from './errorModel'
+
+export const GRAPHQL_FIELD_ERROR_GLOBAL = 'error' as const
+
+export const GraphQLEnumTypeService = new GraphQLEnumType({
+ name: 'Service',
+ values: SERVICES,
+})
+
+export const GraphQLObjectTypeError = new GraphQLObjectType({
+ name: 'Error',
+ description: 'An error returned by a Supabase service',
+ fields: {
+ code: {
+ type: new GraphQLNonNull(GraphQLString),
+ description:
+ 'The unique code identifying the error. The code is stable, and can be used for string matching during error handling.',
+ },
+ service: {
+ type: new GraphQLNonNull(GraphQLEnumTypeService),
+ description: 'The Supabase service that returns this error.',
+ },
+ httpStatusCode: {
+ type: GraphQLInt,
+ description: 'The HTTP status code returned with this error.',
+ },
+ message: {
+ type: GraphQLString,
+ description:
+ 'A human-readable message describing the error. The message is not stable, and should not be used for string matching during error handling. Use the code instead.',
+ },
+ },
+})
diff --git a/apps/docs/resources/rootSchema.ts b/apps/docs/resources/rootSchema.ts
index 5c543d1e79f..02ea9cc9697 100644
--- a/apps/docs/resources/rootSchema.ts
+++ b/apps/docs/resources/rootSchema.ts
@@ -6,6 +6,7 @@ import {
printSchema,
} from 'graphql'
import { RootQueryTypeResolvers } from '~/__generated__/graphql'
+import { errorRoot } from './error/errorResolver'
import { searchRoot } from './globalSearch/globalSearchResolver'
import { GraphQLObjectTypeGuide } from './guide/guideSchema'
import { GraphQLObjectTypeReferenceCLICommand } from './reference/referenceCLISchema'
@@ -35,6 +36,7 @@ export const rootGraphQLSchema = new GraphQLSchema({
fields: {
...introspectRoot,
...searchRoot,
+ ...errorRoot,
},
}),
types: [
diff --git a/apps/docs/scripts/graphqlSchema.ts b/apps/docs/scripts/graphqlSchema.ts
index 17b03215ed7..2f00e2fa79f 100644
--- a/apps/docs/scripts/graphqlSchema.ts
+++ b/apps/docs/scripts/graphqlSchema.ts
@@ -1,7 +1,10 @@
import { printSchema } from 'graphql'
import fs from 'node:fs'
import path from 'node:path'
-import { rootGraphQLSchema } from '../resources/rootSchema'
+import { fileURLToPath } from 'node:url'
+import { rootGraphQLSchema } from '../resources/rootSchema.js'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
async function generateGraphQLSchema() {
try {
@@ -22,6 +25,6 @@ async function generateGraphQLSchema() {
}
}
-if (require.main === module) {
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
generateGraphQLSchema()
}
diff --git a/apps/docs/scripts/last-changed.ts b/apps/docs/scripts/last-changed.ts
index 456f4d56804..d4892653beb 100644
--- a/apps/docs/scripts/last-changed.ts
+++ b/apps/docs/scripts/last-changed.ts
@@ -11,18 +11,21 @@
* the last Git commit date.
*/
-import 'dotenv/config'
+import _configureDotEnv from './utils/dotenv'
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
import matter from 'gray-matter'
import { createHash } from 'node:crypto'
import { readdirSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
-import { join } from 'node:path'
+import path, { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import { SimpleGit, simpleGit } from 'simple-git'
import { parse } from 'smol-toml'
-import { type Section } from './helpers.mdx'
+import { Section } from './helpers.mdx.js'
+
+const _ = _configureDotEnv
interface Options {
reset: boolean
@@ -46,9 +49,11 @@ type SectionWithChecksum = Omit &
const REQUIRED_ENV_VARS = {
SUPABASE_URL: 'NEXT_PUBLIC_SUPABASE_URL',
- SERVICE_ROLE_KEY: 'SUPABASE_SERVICE_ROLE_KEY',
+ SERVICE_ROLE_KEY: 'SUPABASE_SECRET_KEY',
} as const
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
async function main() {
console.log('Updating content timestamps....')
diff --git a/apps/docs/scripts/llms.ts b/apps/docs/scripts/llms.ts
index 47a1b16b48b..abea8501c76 100644
--- a/apps/docs/scripts/llms.ts
+++ b/apps/docs/scripts/llms.ts
@@ -1,6 +1,8 @@
+import _configureDotEnv from './utils/dotenv'
+
import 'dotenv/config'
import fs from 'node:fs/promises'
-
+import { fileURLToPath } from 'node:url'
import {
fetchCliLibReferenceSource,
fetchCSharpLibReferenceSource,
@@ -11,7 +13,9 @@ import {
fetchPythonLibReferenceSource,
fetchSwiftLibReferenceSource,
type SearchSource,
-} from './search/sources'
+} from './search/sources/index.js'
+
+const _ = _configureDotEnv
interface Source {
title: string
@@ -97,6 +101,6 @@ async function generateLlmsTxt() {
}
}
-if (require.main === module) {
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
generateLlmsTxt()
}
diff --git a/apps/docs/scripts/search/generate-embeddings.ts b/apps/docs/scripts/search/generate-embeddings.ts
index dfb83112fac..66d7bb8f6e0 100644
--- a/apps/docs/scripts/search/generate-embeddings.ts
+++ b/apps/docs/scripts/search/generate-embeddings.ts
@@ -1,10 +1,13 @@
+import _configureDotenv from '../utils/dotenv'
+
import { createClient } from '@supabase/supabase-js'
-import 'dotenv/config'
import { parseArgs } from 'node:util'
import { OpenAI } from 'openai'
import { v4 as uuidv4 } from 'uuid'
-import type { Json, Section } from '../helpers.mdx'
-import { fetchAllSources } from './sources'
+import type { Json, Section } from '../helpers.mdx.js'
+import { fetchAllSources } from './sources/index.js'
+
+const _ = _configureDotenv
const args = parseArgs({
options: {
@@ -18,19 +21,20 @@ async function generateEmbeddings() {
const shouldRefresh = Boolean(args.values.refresh)
const requiredEnvVars = [
+ 'DOCS_GITHUB_APP_ID',
+ 'DOCS_GITHUB_APP_INSTALLATION_ID',
+ 'DOCS_GITHUB_APP_PRIVATE_KEY',
+ 'NEXT_PUBLIC_MISC_ANON_KEY',
+ 'NEXT_PUBLIC_MISC_URL',
'NEXT_PUBLIC_SUPABASE_URL',
- 'SUPABASE_SERVICE_ROLE_KEY',
'OPENAI_API_KEY',
- 'NEXT_PUBLIC_MISC_USE_URL',
- 'NEXT_PUBLIC_MISC_USE_ANON_KEY',
- 'SEARCH_GITHUB_APP_ID',
- 'SEARCH_GITHUB_APP_INSTALLATION_ID',
- 'SEARCH_GITHUB_APP_PRIVATE_KEY',
+ 'SUPABASE_SECRET_KEY',
]
- if (requiredEnvVars.some((name) => !process.env[name])) {
+ const missingEnvVars = requiredEnvVars.filter((name) => !process.env[name])
+ if (missingEnvVars.length > 0) {
throw new Error(
- `Environment variables ${requiredEnvVars.join(
+ `Environment variables ${missingEnvVars.join(
', '
)} are required: skipping embeddings generation`
)
@@ -38,8 +42,7 @@ async function generateEmbeddings() {
const supabaseClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
- // eslint-disable-next-line turbo/no-undeclared-env-vars
- process.env.SUPABASE_SERVICE_ROLE_KEY,
+ process.env.SUPABASE_SECRET_KEY,
{
auth: {
persistSession: false,
diff --git a/apps/docs/scripts/search/sources/base.ts b/apps/docs/scripts/search/sources/base.ts
index 7416f8bdb67..32b5aad9e9c 100644
--- a/apps/docs/scripts/search/sources/base.ts
+++ b/apps/docs/scripts/search/sources/base.ts
@@ -1,4 +1,4 @@
-import type { Json, Section } from '../../helpers.mdx'
+import type { Json, Section } from '../../helpers.mdx.js'
export abstract class BaseLoader {
type: string
diff --git a/apps/docs/scripts/search/sources/github-discussion.ts b/apps/docs/scripts/search/sources/github-discussion.ts
index 8eb158122e1..8c3b9a70adc 100644
--- a/apps/docs/scripts/search/sources/github-discussion.ts
+++ b/apps/docs/scripts/search/sources/github-discussion.ts
@@ -2,7 +2,7 @@ import { createAppAuth } from '@octokit/auth-app'
import { Octokit } from '@octokit/core'
import { paginateGraphql } from '@octokit/plugin-paginate-graphql'
import crypto, { createHash } from 'node:crypto'
-import { BaseLoader, BaseSource } from './base'
+import { BaseLoader, BaseSource } from './base.js'
export const ExtendedOctokit = Octokit.plugin(paginateGraphql)
export type ExtendedOctokit = InstanceType
@@ -25,11 +25,9 @@ export type DiscussionsResponse = {
}
}
-const appId = process.env.SEARCH_GITHUB_APP_ID ?? process.env.DOCS_GITHUB_APP_ID
-const installationId =
- process.env.SEARCH_GITHUB_APP_INSTALLATION_ID ?? process.env.DOCS_GITHUB_APP_INSTALLATION_ID
-const privateKey =
- process.env.SEARCH_GITHUB_APP_PRIVATE_KEY ?? process.env.DOCS_GITHUB_APP_PRIVATE_KEY
+const appId = process.env.DOCS_GITHUB_APP_ID
+const installationId = process.env.DOCS_GITHUB_APP_INSTALLATION_ID
+const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY
/**
* Fetches GitHub discussions for a repository + category
diff --git a/apps/docs/scripts/search/sources/index.ts b/apps/docs/scripts/search/sources/index.ts
index 5d02b68421b..9f07c9c72be 100644
--- a/apps/docs/scripts/search/sources/index.ts
+++ b/apps/docs/scripts/search/sources/index.ts
@@ -4,9 +4,9 @@ import {
GitHubDiscussionLoader,
type GitHubDiscussionSource,
fetchDiscussions,
-} from './github-discussion'
-import { MarkdownLoader, type MarkdownSource } from './markdown'
-import { IntegrationLoader, type IntegrationSource, fetchPartners } from './partner-integrations'
+} from './github-discussion.js'
+import { MarkdownLoader, type MarkdownSource } from './markdown.js'
+import { IntegrationLoader, type IntegrationSource, fetchPartners } from './partner-integrations.js'
import {
CliReferenceLoader,
type CliReferenceSource,
@@ -14,8 +14,8 @@ import {
type ClientLibReferenceSource,
OpenApiReferenceLoader,
type OpenApiReferenceSource,
-} from './reference-doc'
-import { walk } from './util'
+} from './reference-doc.js'
+import { walk } from './util.js'
const ignoredFiles = ['pages/404.mdx']
diff --git a/apps/docs/scripts/search/sources/markdown.ts b/apps/docs/scripts/search/sources/markdown.ts
index a34e77c5a97..dcf2d1db722 100644
--- a/apps/docs/scripts/search/sources/markdown.ts
+++ b/apps/docs/scripts/search/sources/markdown.ts
@@ -1,6 +1,6 @@
import { readFile } from 'fs/promises'
-import { processMdx } from '../../helpers.mdx'
-import { BaseLoader, BaseSource } from './base'
+import { processMdx } from '../../helpers.mdx.js'
+import { BaseLoader, BaseSource } from './base.js'
export class MarkdownLoader extends BaseLoader {
type = 'markdown' as const
diff --git a/apps/docs/scripts/search/sources/partner-integrations.ts b/apps/docs/scripts/search/sources/partner-integrations.ts
index 76d78e172a2..742e1367990 100644
--- a/apps/docs/scripts/search/sources/partner-integrations.ts
+++ b/apps/docs/scripts/search/sources/partner-integrations.ts
@@ -1,17 +1,15 @@
import { type SupabaseClient, createClient } from '@supabase/supabase-js'
-import { upperFirst } from 'lodash'
-
-import { BaseLoader, BaseSource } from './base'
-import { processMdx } from '../../helpers.mdx'
+import { upperFirst } from 'lodash-es'
+import { processMdx } from '../../helpers.mdx.js'
+import { BaseLoader, BaseSource } from './base.js'
type PartnerData = {
slug: string // The partner slug corresponding to the last part of the URL
overview: string // The Markdown content for indexing
}
-const supabaseUrl = process.env.NEXT_PUBLIC_MISC_USE_URL ?? process.env.NEXT_PUBLIC_MISC_URL
-const supabaseAnonKey =
- process.env.NEXT_PUBLIC_MISC_USE_ANON_KEY ?? process.env.NEXT_PUBLIC_MISC_ANON_KEY
+const supabaseUrl = process.env.NEXT_PUBLIC_MISC_URL
+const supabaseAnonKey = process.env.NEXT_PUBLIC_MISC_ANON_KEY
let supabaseClient: SupabaseClient
function getSupabaseClient() {
diff --git a/apps/docs/scripts/search/sources/reference-doc.ts b/apps/docs/scripts/search/sources/reference-doc.ts
index 11315fd1c22..047017ace0f 100644
--- a/apps/docs/scripts/search/sources/reference-doc.ts
+++ b/apps/docs/scripts/search/sources/reference-doc.ts
@@ -7,12 +7,12 @@ import type {
ICommonSection,
IFunctionDefinition,
ISpec,
-} from '../../../components/reference/Reference.types'
-import type { CliCommand, CliSpec } from '../../../generator/types/CliSpec'
-import { flattenSections } from '../../../lib/helpers'
-import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers'
-import type { Json } from '../../helpers.mdx'
-import { BaseLoader, BaseSource } from './base'
+} from '../../../components/reference/Reference.types.js'
+import type { CliCommand, CliSpec } from '../../../generator/types/CliSpec.js'
+import { flattenSections } from '../../../lib/helpers.js'
+import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers.js'
+import type { Json } from '../../helpers.mdx.js'
+import { BaseLoader, BaseSource } from './base.js'
export abstract class ReferenceLoader extends BaseLoader {
type = 'reference' as const
diff --git a/apps/docs/scripts/tsconfig.json b/apps/docs/scripts/tsconfig.json
index de1ecb67a32..821a210c5e5 100644
--- a/apps/docs/scripts/tsconfig.json
+++ b/apps/docs/scripts/tsconfig.json
@@ -1,6 +1,8 @@
// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig
{
"compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
"incremental": true,
"noImplicitAny": false,
"esModuleInterop": true,
diff --git a/apps/docs/scripts/utils/dotenv.ts b/apps/docs/scripts/utils/dotenv.ts
new file mode 100644
index 00000000000..6a826531068
--- /dev/null
+++ b/apps/docs/scripts/utils/dotenv.ts
@@ -0,0 +1,5 @@
+import dotenv from 'dotenv'
+
+dotenv.config({ path: '.env.local' })
+
+export default undefined
diff --git a/apps/docs/spec/common-client-libs-sections.json b/apps/docs/spec/common-client-libs-sections.json
index a9206ccf0c4..b7087b1ef69 100644
--- a/apps/docs/spec/common-client-libs-sections.json
+++ b/apps/docs/spec/common-client-libs-sections.json
@@ -791,6 +791,12 @@
"type": "category",
"title": "Realtime",
"items": [
+ {
+ "id": "realtime-api",
+ "title": "Overview",
+ "slug": "realtime-api",
+ "type": "function"
+ },
{
"id": "stream",
"title": "Listen to database changes",
diff --git a/apps/docs/spec/supabase_py_v2.yml b/apps/docs/spec/supabase_py_v2.yml
index 90862db77d1..e582bcd3cce 100644
--- a/apps/docs/spec/supabase_py_v2.yml
+++ b/apps/docs/spec/supabase_py_v2.yml
@@ -97,7 +97,7 @@ functions:
url: str = os.environ.get("SUPABASE_URL")
key: str = os.environ.get("SUPABASE_KEY")
supabase: Client = create_client(
- url,
+ url,
key,
options=ClientOptions(
postgrest_client_timeout=10,
@@ -173,7 +173,7 @@ functions:
```python
response = supabase.auth.sign_up(
{
- "email": "email@example.com",
+ "email": "email@example.com",
"password": "password",
}
)
@@ -281,7 +281,7 @@ functions:
```python
response = supabase.auth.sign_up(
{
- "phone": "123456789",
+ "phone": "123456789",
"password": "password",
}
)
@@ -467,7 +467,7 @@ functions:
```python
response = supabase.auth.sign_in_with_password(
{
- "email": "email@example.com",
+ "email": "email@example.com",
"password": "example-password",
}
)
@@ -551,7 +551,7 @@ functions:
```python
response = supabase.auth.sign_in_with_password(
{
- "phone": "+13334445555",
+ "phone": "+13334445555",
"password": "some-password",
}
)
@@ -776,7 +776,7 @@ functions:
```python
response = supabase.auth.sign_in_with_id_token(
{
- "provider": "google",
+ "provider": "google",
"token": "your-id-token",
}
)
@@ -1000,8 +1000,8 @@ functions:
```python
response = supabase.auth.verify_otp(
{
- "email": "email@example.com",
- "token": "123456",
+ "email": "email@example.com",
+ "token": "123456",
"type": "email",
}
)
@@ -1081,8 +1081,8 @@ functions:
```python
response = supabase.auth.verify_otp(
{
- "phone": "+13334445555",
- "token": "123456",
+ "phone": "+13334445555",
+ "token": "123456",
"type": "sms",
}
)
@@ -1094,8 +1094,8 @@ functions:
```python
response = supabase.auth.verify_otp(
{
- "email": "email@example.com",
- "token_hash": "",
+ "email": "email@example.com",
+ "token_hash": "",
"type": "email",
}
)
@@ -5531,8 +5531,8 @@ functions:
supabase.table("texts")
.select("content")
.text_search(
- "content",
- "'eggs' & 'ham'",
+ "content",
+ "'eggs' & 'ham'",
options={"config": "english"},
)
.execute()
@@ -6650,7 +6650,7 @@ functions:
code: |
```python
response = supabase.functions.invoke(
- "hello-world",
+ "hello-world",
invoke_options={
"body": {"name": "Functions"},
},
@@ -6703,6 +6703,32 @@ functions:
)
```
+ - id: realtime-api
+ title: 'Overview'
+ description: |
+ Realtime in Python only works with the asynchronous client.
+ You can initialize a new Supabase client using the `acreate_client()` method.
+
+ - Some Realtime methods are asynchronous and must be awaited. Ensure these methods are called within an `async` function.
+ - In the following Realtime examples, certain methods are awaited. These should be enclosed within an `async` function.
+ - When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the `acreate_client` example includes an import of `asyncio`.
+ examples:
+ - id: acreate-client
+ name: acreate_client()
+ code: |
+ ```python
+ import os
+ import asyncio
+ from supabase import acreate_client, AsyncClient
+
+ url: str = os.environ.get("SUPABASE_URL")
+ key: str = os.environ.get("SUPABASE_KEY")
+
+ async def create_supabase():
+ supabase: AsyncClient = await acreate_client(url, key)
+ return supabase
+ ```
+
- id: subscribe
title: on().subscribe()
notes: |
@@ -6720,15 +6746,15 @@ functions:
def on_subscribe(status, err):
if status == RealtimeSubscribeStates.SUBSCRIBED:
- channel.send_broadcast(
- "cursor-pos",
+ asyncio.create_task(channel.send_broadcast(
+ "cursor-pos",
{"x": random.random(), "y": random.random()}
- )
+ ))
def handle_broadcast(payload):
print("Cursor position received!", payload)
- channel.on_broadcast(event="cursor-pos", callback=handle_broadcast).subscribe(on_subscribe)
+ await channel.on_broadcast(event="cursor-pos", callback=handle_broadcast).subscribe(on_subscribe)
```
- id: listen-to-presence-sync
name: Listen to presence sync
@@ -6739,12 +6765,12 @@ functions:
def on_subscribe(status, err):
if status == RealtimeSubscribeStates.SUBSCRIBED:
- channel.track({"online_at": datetime.datetime.now().isoformat()})
+ asyncio.create_task(channel.track({"online_at": datetime.datetime.now().isoformat()}))
def handle_presence_sync():
print("Synced presence state: ", channel.presence.state)
- channel.on_presence_sync(callback=handle_presence_sync).subscribe(on_subscribe)
+ await channel.on_presence_sync(callback=handle_presence_sync).subscribe(on_subscribe)
```
- id: listen-to-presence-join
name: Listen to presence join
@@ -6758,9 +6784,9 @@ functions:
def on_subscribe(status, err):
if status == RealtimeSubscribeStates.SUBSCRIBED:
- channel.track({"online_at": datetime.datetime.now().isoformat()})
+ asyncio.create_task(channel.track({"online_at": datetime.datetime.now().isoformat()}))
- channel.on_presence_join(callback=handle_presence_join).subscribe(on_subscribe)
+ await channel.on_presence_join(callback=handle_presence_join).subscribe(on_subscribe)
```
- id: listen-to-presence-leave
name: Listen to presence leave
@@ -6774,10 +6800,10 @@ functions:
def on_subscribe(status, err):
if status == RealtimeSubscribeStates.SUBSCRIBED:
- channel.track({"online_at": datetime.datetime.now().isoformat()})
- channel.untrack()
+ asyncio.create_task(channel.track({"online_at": datetime.datetime.now().isoformat()}))
+ asyncio.create_task(channel.untrack())
- channel.on_presence_leave(callback=handle_presence_leave).subscribe(on_subscribe)
+ await channel.on_presence_leave(callback=handle_presence_leave).subscribe(on_subscribe)
```
- id: listen-to-all-database-changes
name: Listen to all database changes
@@ -6785,7 +6811,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("*", schema="*", callback=handle_record_updated)
.subscribe()
)
@@ -6795,7 +6821,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("*", schema="public", table="countries", callback=handle_record_updated)
.subscribe()
)
@@ -6805,7 +6831,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("INSERT", schema="public", table="countries", callback=handle_record_inserted)
.subscribe()
)
@@ -6822,7 +6848,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("UPDATE", schema="public", table="countries", callback=handle_record_updated)
.subscribe()
)
@@ -6839,7 +6865,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("DELETE", schema="public", table="countries", callback=handle_record_deleted)
.subscribe()
)
@@ -6850,7 +6876,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("INSERT", schema="public", table="countries", callback=handle_record_inserted)
.on_postgres_changes("DELETE", schema="public", table="countries", callback=handle_record_deleted)
.subscribe()
@@ -6864,7 +6890,7 @@ functions:
code: |
```python
response = (
- supabase.channel("room1")
+ await supabase.channel("room1")
.on_postgres_changes("UPDATE", schema="public", table="countries", filter="id=eq.200", callback=handle_record_updated)
.subscribe()
)
@@ -6883,9 +6909,9 @@ functions:
def on_subscribe(status, err):
if status == RealtimeSubscribeStates.SUBSCRIBED:
- channel.send_broadcast('cursor-pos', {"x": random.random(), "y": random.random()})
+ asyncio.create_task(channel.send_broadcast('cursor-pos', {"x": random.random(), "y": random.random()}))
- channel.subscribe(on_subscribe)
+ await channel.subscribe(on_subscribe)
```
response: |
```
@@ -6912,7 +6938,7 @@ functions:
isSpotlight: true
code: |
```python
- supabase.remove_channel(myChannel)
+ await supabase.remove_channel(myChannel)
```
- id: remove-all-channels
@@ -6925,7 +6951,7 @@ functions:
isSpotlight: true
code: |
```python
- supabase.remove_all_channels()
+ await supabase.remove_all_channels()
```
- id: list-buckets
@@ -7305,7 +7331,7 @@ functions:
supabase.storage
.from_("avatars")
.move(
- "public/avatar1.png",
+ "public/avatar1.png",
"private/avatar2.png"
)
)
@@ -7342,7 +7368,7 @@ functions:
supabase.storage
.from_("avatars")
.copy(
- "public/avatar1.png",
+ "public/avatar1.png",
"private/avatar2.png"
)
)
@@ -7391,7 +7417,7 @@ functions:
supabase.storage
.from_("avatars")
.create_signed_urls(
- ["folder/avatar1.png", "folder/avatar2.png"],
+ ["folder/avatar1.png", "folder/avatar2.png"],
60
)
)
@@ -7475,7 +7501,7 @@ functions:
supabase.storage
.from_("avatars")
.create_signed_url(
- "folder/avatar1.png",
+ "folder/avatar1.png",
60
)
)
@@ -7495,8 +7521,8 @@ functions:
supabase.storage
.from_("avatars")
.create_signed_url(
- "folder/avatar1.png",
- 60,
+ "folder/avatar1.png",
+ 60,
{"transform": {"width": 100, "height": 100}}
)
)
@@ -7510,8 +7536,8 @@ functions:
supabase.storage
.from_("avatars")
.create_signed_url(
- "folder/avatar1.png",
- 60,
+ "folder/avatar1.png",
+ 60,
{"download": True}
)
)
@@ -7691,7 +7717,7 @@ functions:
supabase.storage
.from_("avatars")
.get_public_url(
- "folder/avatar1.jpg",
+ "folder/avatar1.jpg",
{"transform": {"width": 100, "height": 100}}
)
)
@@ -7705,7 +7731,7 @@ functions:
supabase.storage
.from_("avatars")
.get_public_url(
- "folder/avatar1.jpg",
+ "folder/avatar1.jpg",
{"download": True}
)
)
diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json
index 3eed84d3fe6..41bf9f6455c 100644
--- a/apps/docs/turbo.json
+++ b/apps/docs/turbo.json
@@ -17,9 +17,7 @@
"NEXT_PUBLIC_SUPABASE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
"NEXT_PUBLIC_MISC_URL",
- "NEXT_PUBLIC_MISC_USE_URL",
"NEXT_PUBLIC_MISC_ANON_KEY",
- "NEXT_PUBLIC_MISC_USE_ANON_KEY",
"NODE_ENV",
"NEXT_PUBLIC_API_URL",
"NEXT_PUBLIC_BASE_PATH",
@@ -48,9 +46,6 @@
"DOCS_REVALIDATION_OVERRIDE_KEYS",
"FORCE_ASSET_CDN",
"OPENAI_API_KEY",
- "SEARCH_GITHUB_APP_ID",
- "SEARCH_GITHUB_APP_INSTALLATION_ID",
- "SEARCH_GITHUB_APP_PRIVATE_KEY",
"SITE_NAME",
"SUPABASE_SECRET_KEY"
],
diff --git a/apps/docs/vitest.config.ts b/apps/docs/vitest.config.ts
index b0047198db1..bbadb9591ec 100644
--- a/apps/docs/vitest.config.ts
+++ b/apps/docs/vitest.config.ts
@@ -1,9 +1,10 @@
-import { defineConfig } from 'vitest/config'
import tsconfigPaths from 'vite-tsconfig-paths'
+import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
exclude: ['examples/**/*', '**/node_modules/**'],
+ setupFiles: ['vitest.setup.ts'],
},
plugins: [tsconfigPaths({ root: import.meta.dirname })],
})
diff --git a/apps/docs/vitest.setup.ts b/apps/docs/vitest.setup.ts
new file mode 100644
index 00000000000..ef03c799e6a
--- /dev/null
+++ b/apps/docs/vitest.setup.ts
@@ -0,0 +1,24 @@
+import { afterAll, beforeAll, vi } from 'vitest'
+
+let oldEnv: NodeJS.ProcessEnv
+
+beforeAll(() => {
+ // Use local Supabase to run e2e tests
+ oldEnv = { ...process.env }
+ process.env = {
+ ...process.env,
+ NEXT_PUBLIC_SUPABASE_URL: 'http://localhost:54321',
+ NEXT_PUBLIC_SUPABASE_ANON_KEY:
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0',
+ }
+
+ // Prevent errors about importing server-only modules from Client Components
+ vi.mock('server-only', () => {
+ return {}
+ })
+})
+
+afterAll(() => {
+ process.env = oldEnv
+ vi.doUnmock('server-only')
+})
diff --git a/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx
index 0d9a97bad5f..5e33bcbc7a5 100644
--- a/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx
+++ b/apps/studio/components/interfaces/Connect/content/remix/supabasejs/content.tsx
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
-SUPABASE_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
+SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx
index 6a8dc557694..ae81db255ce 100644
--- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx
+++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx
@@ -1,6 +1,6 @@
import type { PostgresSchema } from '@supabase/postgres-meta'
import { toPng, toSvg } from 'html-to-image'
-import { Download, Loader2 } from 'lucide-react'
+import { Check, Download, Loader2, Clipboard, Info } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useEffect, useMemo, useState } from 'react'
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
@@ -21,7 +21,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { SchemaGraphLegend } from './SchemaGraphLegend'
import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.utils'
import { TableNode } from './SchemaTableNode'
-
+import { copyToClipboard } from 'ui'
// [Joshen] Persisting logic: Only save positions to local storage WHEN a node is moved OR when explicitly clicked to reset layout
export const SchemaGraph = () => {
@@ -30,6 +30,13 @@ export const SchemaGraph = () => {
const { project } = useProjectContext()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
+ const [copied, setCopied] = useState(false)
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => setCopied(false), 2000)
+ }
+ }, [copied])
+
const [isDownloading, setIsDownloading] = useState(false)
const miniMapNodeColor = '#111318'
@@ -160,6 +167,59 @@ export const SchemaGraph = () => {
}
}
+ function tablesToSQL(t: typeof tables) {
+ if (!Array.isArray(t)) return ''
+ const warning =
+ '-- WARNING: This schema is for context only and is not meant to be run.\n-- Table order and constraints may not be valid for execution.\n\n'
+ const sql = t
+ .map((table) => {
+ if (!table || !Array.isArray((table as any).columns)) return ''
+
+ const columns = (table as { columns?: any[] }).columns ?? []
+ const columnLines = columns.map((c) => {
+ let line = ` ${c.name} ${c.data_type}`
+ if (c.is_identity) {
+ line += ' GENERATED ALWAYS AS IDENTITY'
+ }
+ if (c.is_nullable === false) {
+ line += ' NOT NULL'
+ }
+ if (c.default_value !== null && c.default_value !== undefined) {
+ line += ` DEFAULT ${c.default_value}`
+ }
+ if (c.is_unique) {
+ line += ' UNIQUE'
+ }
+ if (c.check) {
+ line += ` CHECK (${c.check})`
+ }
+ return line
+ })
+
+ const constraints: string[] = []
+
+ if (Array.isArray(table.primary_keys) && table.primary_keys.length > 0) {
+ const pkCols = table.primary_keys.map((pk) => pk.name).join(', ')
+ constraints.push(` CONSTRAINT ${table.name}_pkey PRIMARY KEY (${pkCols})`)
+ }
+
+ if (Array.isArray(table.relationships)) {
+ table.relationships.forEach((rel) => {
+ if (rel && rel.source_table_name === table.name) {
+ constraints.push(
+ ` CONSTRAINT ${rel.constraint_name} FOREIGN KEY (${rel.source_column_name}) REFERENCES ${rel.target_table_schema}.${rel.target_table_name}(${rel.target_column_name})`
+ )
+ }
+ })
+ }
+
+ const allLines = [...columnLines, ...constraints]
+ return `CREATE TABLE ${table.schema}.${table.name} (\n${allLines.join(',\n')}\n);`
+ })
+ .join('\n')
+ return warning + sql
+ }
+
useEffect(() => {
if (isSuccessTables && isSuccessSchemas && tables.length > 0) {
const schema = schemas.find((s) => s.name === selectedSchema) as PostgresSchema
@@ -192,6 +252,40 @@ export const SchemaGraph = () => {
onSelectSchema={setSelectedSchema}
/>