diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index aa0e0fd0c157f..8dd65a5ca2e73 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -123,6 +123,22 @@ const pageMap = [ }, remoteFile: 'bigquery.md', }, + { + slug: 'cal', + meta: { + title: 'Cal.com', + dashboardIntegrationPath: 'cal_wrapper', + }, + remoteFile: 'cal.md', + }, + { + slug: 'calendly', + meta: { + title: 'Calendly', + dashboardIntegrationPath: 'calendly_wrapper', + }, + remoteFile: 'calendly.md', + }, { slug: 'clerk', meta: { @@ -139,6 +155,14 @@ const pageMap = [ }, remoteFile: 'clickhouse.md', }, + { + slug: 'cloudflare-d1', + meta: { + title: 'Cloudflare D1', + dashboardIntegrationPath: 'cfd1_wrapper', + }, + remoteFile: 'cfd1.md', + }, { slug: 'cognito', meta: { @@ -151,9 +175,18 @@ const pageMap = [ slug: 'duckdb', meta: { title: 'DuckDB', + dashboardIntegrationPath: undefined, }, remoteFile: 'duckdb.md', }, + { + slug: 'dynamodb', + meta: { + title: 'AWS DynamoDB', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'dynamodb.md', + }, { slug: 'firebase', meta: { @@ -162,6 +195,22 @@ const pageMap = [ }, remoteFile: 'firebase.md', }, + { + slug: 'gravatar', + meta: { + title: 'Gravatar', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'gravatar.md', + }, + { + slug: 'hubspot', + meta: { + title: 'HubSpot', + dashboardIntegrationPath: 'hubspot_wrapper', + }, + remoteFile: 'hubspot.md', + }, { slug: 'iceberg', meta: { @@ -170,6 +219,14 @@ const pageMap = [ }, remoteFile: 'iceberg.md', }, + { + slug: 'infura', + meta: { + title: 'Infura', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'infura.md', + }, { slug: 'logflare', meta: { @@ -186,6 +243,14 @@ const pageMap = [ }, remoteFile: 'mssql.md', }, + { + slug: 'mysql', + meta: { + title: 'MySQL', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'mysql.md', + }, { slug: 'notion', meta: { @@ -194,6 +259,22 @@ const pageMap = [ }, remoteFile: 'notion.md', }, + { + slug: 'openapi', + meta: { + title: 'OpenAPI', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'openapi.md', + }, + { + slug: 'orb', + meta: { + title: 'Orb', + dashboardIntegrationPath: 'orb_wrapper', + }, + remoteFile: 'orb.md', + }, { slug: 'paddle', meta: { @@ -226,6 +307,22 @@ const pageMap = [ }, remoteFile: 's3vectors.md', }, + { + slug: 'shopify', + meta: { + title: 'Shopify', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'shopify.md', + }, + { + slug: 'slack', + meta: { + title: 'Slack', + dashboardIntegrationPath: undefined, + }, + remoteFile: 'slack.md', + }, { slug: 'snowflake', meta: { diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 2b44092ef3abc..86b77a828768e 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1331,76 +1331,130 @@ export const database: NavMenuConstant = { url: '/guides/database/extensions/wrappers/overview' as `/${string}`, }, { - name: 'Connecting to Auth0', - url: '/guides/database/extensions/wrappers/auth0' as `/${string}`, - }, - { - name: 'Connecting to Airtable', - url: '/guides/database/extensions/wrappers/airtable' as `/${string}`, - }, - { - name: 'Connecting to AWS Cognito', - url: '/guides/database/extensions/wrappers/cognito' as `/${string}`, - }, - { - name: 'Connecting to AWS S3', - url: '/guides/database/extensions/wrappers/s3' as `/${string}`, - }, - { - name: 'Connecting to AWS S3 Vectors', - url: '/guides/database/extensions/wrappers/s3_vectors' as `/${string}`, - }, - { - name: 'Connecting to BigQuery', - url: '/guides/database/extensions/wrappers/bigquery' as `/${string}`, - }, - { - name: 'Connecting to Clerk', - url: '/guides/database/extensions/wrappers/clerk' as `/${string}`, - }, - { - name: 'Connecting to ClickHouse', - url: '/guides/database/extensions/wrappers/clickhouse' as `/${string}`, - }, - { - name: 'Connecting to DuckDB', - url: '/guides/database/extensions/wrappers/duckdb' as `/${string}`, - }, - { - name: 'Connecting to Firebase', - url: '/guides/database/extensions/wrappers/firebase' as `/${string}`, - }, - { - name: 'Connecting to Iceberg', - url: '/guides/database/extensions/wrappers/iceberg' as `/${string}`, - }, - { - name: 'Connecting to Logflare', - url: '/guides/database/extensions/wrappers/logflare' as `/${string}`, - }, - { - name: 'Connecting to MSSQL', - url: '/guides/database/extensions/wrappers/mssql' as `/${string}`, - }, - { - name: 'Connecting to Notion', - url: '/guides/database/extensions/wrappers/notion' as `/${string}`, - }, - { - name: 'Connecting to Paddle', - url: '/guides/database/extensions/wrappers/paddle' as `/${string}`, - }, - { - name: 'Connecting to Redis', - url: '/guides/database/extensions/wrappers/redis' as `/${string}`, - }, - { - name: 'Connecting to Snowflake', - url: '/guides/database/extensions/wrappers/snowflake' as `/${string}`, - }, - { - name: 'Connecting to Stripe', - url: '/guides/database/extensions/wrappers/stripe' as `/${string}`, + name: 'Sources', + url: '/guides/database/extensions/wrappers/overview' as `/${string}`, + items: [ + { + name: 'Airtable', + url: '/guides/database/extensions/wrappers/airtable' as `/${string}`, + }, + { + name: 'Auth0', + url: '/guides/database/extensions/wrappers/auth0' as `/${string}`, + }, + { + name: 'AWS Cognito', + url: '/guides/database/extensions/wrappers/cognito' as `/${string}`, + }, + { + name: 'AWS DynamoDB', + url: '/guides/database/extensions/wrappers/dynamodb' as `/${string}`, + }, + { + name: 'AWS S3', + url: '/guides/database/extensions/wrappers/s3' as `/${string}`, + }, + { + name: 'AWS S3 Vectors', + url: '/guides/database/extensions/wrappers/s3_vectors' as `/${string}`, + }, + { + name: 'BigQuery', + url: '/guides/database/extensions/wrappers/bigquery' as `/${string}`, + }, + { + name: 'Cal.com', + url: '/guides/database/extensions/wrappers/cal' as `/${string}`, + }, + { + name: 'Calendly', + url: '/guides/database/extensions/wrappers/calendly' as `/${string}`, + }, + { + name: 'Clerk', + url: '/guides/database/extensions/wrappers/clerk' as `/${string}`, + }, + { + name: 'ClickHouse', + url: '/guides/database/extensions/wrappers/clickhouse' as `/${string}`, + }, + { + name: 'Cloudflare D1', + url: '/guides/database/extensions/wrappers/cloudflare-d1' as `/${string}`, + }, + { + name: 'DuckDB', + url: '/guides/database/extensions/wrappers/duckdb' as `/${string}`, + }, + { + name: 'Firebase', + url: '/guides/database/extensions/wrappers/firebase' as `/${string}`, + }, + { + name: 'Gravatar', + url: '/guides/database/extensions/wrappers/gravatar' as `/${string}`, + }, + { + name: 'HubSpot', + url: '/guides/database/extensions/wrappers/hubspot' as `/${string}`, + }, + { + name: 'Iceberg', + url: '/guides/database/extensions/wrappers/iceberg' as `/${string}`, + }, + { + name: 'Infura', + url: '/guides/database/extensions/wrappers/infura' as `/${string}`, + }, + { + name: 'Logflare', + url: '/guides/database/extensions/wrappers/logflare' as `/${string}`, + }, + { + name: 'MSSQL', + url: '/guides/database/extensions/wrappers/mssql' as `/${string}`, + }, + { + name: 'MySQL', + url: '/guides/database/extensions/wrappers/mysql' as `/${string}`, + }, + { + name: 'Notion', + url: '/guides/database/extensions/wrappers/notion' as `/${string}`, + }, + { + name: 'OpenAPI', + url: '/guides/database/extensions/wrappers/openapi' as `/${string}`, + }, + { + name: 'Orb', + url: '/guides/database/extensions/wrappers/orb' as `/${string}`, + }, + { + name: 'Paddle', + url: '/guides/database/extensions/wrappers/paddle' as `/${string}`, + }, + { + name: 'Redis', + url: '/guides/database/extensions/wrappers/redis' as `/${string}`, + }, + { + name: 'Shopify', + url: '/guides/database/extensions/wrappers/shopify' as `/${string}`, + }, + { + name: 'Slack', + url: '/guides/database/extensions/wrappers/slack' as `/${string}`, + }, + { + name: 'Snowflake', + url: '/guides/database/extensions/wrappers/snowflake' as `/${string}`, + }, + { + name: 'Stripe', + url: '/guides/database/extensions/wrappers/stripe' as `/${string}`, + }, + ], }, ], }, @@ -2681,6 +2735,10 @@ export const platform: NavMenuConstant = { name: 'Network Restrictions', url: '/guides/platform/network-restrictions' as `/${string}`, }, + { + name: 'Temporary Access', + url: '/guides/platform/temporary-access' as `/${string}`, + }, { name: 'Performance Tuning', url: '/guides/platform/performance' as `/${string}` }, { name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' as `/${string}` }, { diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index fa6fad89c0c23..4fb8c15b66c40 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -31,15 +31,72 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {aiExamples.map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + A toolkit to perform vector similarity search on your knowledge base embeddings. + + +
+
+ + + Implement image search with the OpenAI CLIP Model and Supabase Vector. + + +
+
+ + + Generate image captions using Hugging Face. + + +
+
+ + + Generate GPT text completions using OpenAI in Edge Functions. + + +
+
+ + + Use Supabase as a Retrieval Store for your ChatGPT plugin. + + +
+
+ + + Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase. + + +
{/* */} @@ -49,13 +106,45 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {aiIntegrations.map((x) => ( -
- - {x.description} - -
- ))} +
+ + + OpenAI is an AI research and deployment company. Supabase provides a simple way to use + OpenAI in your applications. + + +
+
+ + + A fully managed service that offers a choice of high-performing foundation models from + leading AI companies. + + +
+
+ + + Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way + to use Hugging Face's models in your applications. + + +
+
+ + + LangChain is a language-agnostic, open-source, and self-hosted API for text translation, + summarization, and sentiment analysis. + + +
+
+ + + LlamaIndex is a data framework for your LLM applications. + + +
{/* */} @@ -65,32 +154,31 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
- {[ - { - name: 'Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector', - description: - 'Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.', - href: 'https://supabase.com/customers/berriai', - }, - { - name: 'Firecrawl switches from Pinecone to Supabase for Postgres vector embeddings', - description: - 'How Firecrawl boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector', - href: 'https://supabase.com/customers/firecrawl', - }, - { - name: 'Markprompt: GDPR-Compliant AI Chatbots for Docs and Websites', - description: - "AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions", - href: 'https://supabase.com/customers/markprompt', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + + Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS + and successfully migrated to Supabase. + + +
+
+ + + How Firecrawl boosts efficiency and accuracy of chat powered search for documentation using + Supabase with pgvector + + +
+
+ + + AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and + GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure + and privacy-focused database and authentication solutions + + +
{/* */} diff --git a/apps/docs/content/guides/auth/auth-hooks.mdx b/apps/docs/content/guides/auth/auth-hooks.mdx index 94471ff88032e..bc0a4d34ede33 100644 --- a/apps/docs/content/guides/auth/auth-hooks.mdx +++ b/apps/docs/content/guides/auth/auth-hooks.mdx @@ -366,37 +366,39 @@ Outside of runtime errors, both HTTP Hooks and Postgres Hooks return timeout err Each Hook description contains an example JSON Schema which you can use in conjunction with [JSON Schema Faker](https://json-schema-faker.js.org/) in order to generate a mock payload. For HTTP Hooks, you can also use [the Standard Webhooks Testing Tool](https://www.standardwebhooks.com/simulate) to simulate a request.
- {[ - { - name: 'Custom Access Token', - description: 'Customize the access token issued by Supabase Auth', - href: '/guides/auth/auth-hooks/custom-access-token-hook', - }, - { - name: 'Send SMS', - description: 'Use a custom SMS provider to send authentication messages', - href: '/guides/auth/auth-hooks/send-sms-hook', - }, - { - name: 'Send Email', - description: 'Use a custom email provider to send authentication messages', - href: '/guides/auth/auth-hooks/send-email-hook', - }, - { - name: 'MFA Verification', - description: 'Add additional checks to the MFA verification flow', - href: '/guides/auth/auth-hooks/mfa-verification-hook', - }, - { - name: 'Password verification', - description: 'Add additional checks to the password verification flow', - href: '/guides/auth/auth-hooks/password-verification-hook', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + + Customize the access token issued by Supabase Auth + + +
+
+ + + Use a custom SMS provider to send authentication messages + + +
+
+ + + Use a custom email provider to send authentication messages + + +
+
+ + + Add additional checks to the MFA verification flow + + +
+
+ + + Add additional checks to the password verification flow + + +
diff --git a/apps/docs/content/guides/auth/oauth-server.mdx b/apps/docs/content/guides/auth/oauth-server.mdx index f6b5c9736cee8..31df7fae5fced 100644 --- a/apps/docs/content/guides/auth/oauth-server.mdx +++ b/apps/docs/content/guides/auth/oauth-server.mdx @@ -54,35 +54,34 @@ OAuth 2.1 Server works seamlessly with your existing Supabase Auth configuration To enable OAuth 2.1 Server in your project, follow these guides:
- {[ - { - name: 'Getting Started', - description: - 'Enable OAuth 2.1, configure your authorization endpoint, and register your first client.', - href: '/guides/auth/oauth-server/getting-started', - }, - { - name: 'OAuth Flows', - description: 'Detailed walkthrough of authorization code and refresh token flows.', - href: '/guides/auth/oauth-server/oauth-flows', - }, - { - name: 'MCP Authentication', - description: 'Authenticate AI agents and LLM tools using Model Context Protocol.', - href: '/guides/auth/oauth-server/mcp-authentication', - }, - { - name: 'Token Security & RLS', - description: 'Control data access with Row Level Security policies for OAuth clients.', - href: '/guides/auth/oauth-server/token-security', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + + Enable OAuth 2.1, configure your authorization endpoint, and register your first client. + + +
+
+ + + Detailed walkthrough of authorization code and refresh token flows. + + +
+
+ + + Authenticate AI agents and LLM tools using Model Context Protocol. + + +
+
+ + + Control data access with Row Level Security policies for OAuth clients. + + +
## Resources diff --git a/apps/docs/content/guides/auth/server-side.mdx b/apps/docs/content/guides/auth/server-side.mdx index d0fdc6cd10828..782db4bc501e6 100644 --- a/apps/docs/content/guides/auth/server-side.mdx +++ b/apps/docs/content/guides/auth/server-side.mdx @@ -20,28 +20,16 @@ We have developed an [`@supabase/ssr`](https://www.npmjs.com/package/@supabase/s ## Framework quickstarts
- {[ - { - title: 'Next.js', - href: '/guides/auth/server-side/nextjs', - description: - 'Automatically configure Supabase in Next.js to use cookies, making your user and their session available on the client and server.', - icon: '/docs/img/icons/nextjs-icon', - }, - { - title: 'SvelteKit', - href: '/guides/auth/server-side/sveltekit', - description: - 'Automatically configure Supabase in SvelteKit to use cookies, making your user and their session available on the client and server.', - icon: '/docs/img/icons/svelte-icon', - }, - ].map((item) => { - return ( - - - {item.description} - - - ) - })} + + + Automatically configure Supabase in Next.js to use cookies, making your user and their session + available on the client and server. + + + + + Automatically configure Supabase in SvelteKit to use cookies, making your user and their + session available on the client and server. + +
diff --git a/apps/docs/content/guides/cli.mdx b/apps/docs/content/guides/cli.mdx index 5c81c2414bfd4..13297eba2c2be 100644 --- a/apps/docs/content/guides/cli.mdx +++ b/apps/docs/content/guides/cli.mdx @@ -13,25 +13,19 @@ The Supabase CLI provides tools to develop your project locally, deploy to the S ## Resources
- {[ - { - name: 'Supabase CLI', - description: - 'The Supabase CLI provides tools to develop manage your Supabase projects from your local machine.', - href: 'https://github.com/supabase/cli', - }, - { - name: 'GitHub Action', - description: ' A GitHub action for interacting with your Supabase projects using the CLI.', - href: 'https://github.com/supabase/setup-cli', - }, - ].map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + The Supabase CLI provides tools to develop manage your Supabase projects from your local + machine. + + +
+
+ + + A GitHub action for interacting with your Supabase projects using the CLI. + + +
diff --git a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx index eac4f7bf93acf..2459167434089 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx @@ -19,46 +19,25 @@ Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integra
- {[ - { - title: 'supabase-js', - hasLightIcon: true, - href: 'https://supabase.link/nextjs-with-supabase-starter', - description: 'A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript and Tailwind CSS.' - }, - /* { TODO: Link the correct next.js template that uses drizzle ORM with supabase database. - hasLightIcon: true, - href: 'https://supabase.link/nextjs-supabase-drizzle', - description: "Simple Next.js template that uses Supabase as the database and Drizzle as the ORM.", - },*/ - { - title: 'Kysely', - hasLightIcon: true, - href: 'https://supabase.link/nextjs-supabase-kysely', - description: 'Simple Next.js template that uses Supabase as the database and Kysely as the query builder.', - }, - /* { TODO: figure out how to get around Prisma accelerate requirement... - title: 'Prisma', - hasLightIcon: true, - href: 'https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fstorage%2Fpostgres-prisma&project-name=postgres-prisma&repository-name=postgres-prisma&demo-title=Vercel%20Postgres%20%2B%20Prisma%20Next.js%20Starter&demo-description=Simple%20Next.js%20template%20that%20uses%20Vercel%20Postgres%20as%20the%20database%20and%20Prisma%20as%20the%20ORM.&demo-url=https%3A%2F%2Fpostgres-prisma.vercel.app%2F&demo-image=https%3A%2F%2Fpostgres-prisma.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6', - description: 'Simple Next.js template that uses Vercel Postgres as the database and Prisma as the ORM.', - } */ - ].map((resource) => { - return ( - - - {resource.description} - - - ) - -})} - + + + A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript + and Tailwind CSS. + + + + + Simple Next.js template that uses Supabase as the database and Kysely as the query builder. + +
@@ -75,9 +54,9 @@ POSTGRES_URL="postgres://postgres.cfcxynqnhdybqtbhjemm:[YOUR-PASSWORD]@aws-0-ap- ```ts lib/drizzle.ts -import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core' -import { InferSelectModel, InferInsertModel } from 'drizzle-orm' import { sql } from '@vercel/postgres' +import { InferInsertModel, InferSelectModel } from 'drizzle-orm' +import { pgTable, serial, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/vercel-postgres' export const UsersTable = pgTable( @@ -107,8 +86,8 @@ export const db = drizzle(sql) ```ts lib/kysely.ts -import { Generated, ColumnType } from 'kysely' import { createKysely } from '@vercel/postgres-kysely' +import { ColumnType, Generated } from 'kysely' interface UserTable { // Columns that are generated by the database should be marked diff --git a/apps/docs/content/guides/database/prisma.mdx b/apps/docs/content/guides/database/prisma.mdx index 31ea39f16e54d..b72c2f8294fd5 100644 --- a/apps/docs/content/guides/database/prisma.mdx +++ b/apps/docs/content/guides/database/prisma.mdx @@ -78,7 +78,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npm init -y - npm install prisma typescript ts-node @types/node --save-dev + npm install prisma tsx @types/pg --save-dev + npm install @prisma/client @prisma/adapter-pg dotenv pg npx tsc --init @@ -87,8 +88,9 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash - pnpm init -y - pnpm install prisma typescript ts-node @types/node --save-dev + pnpm init + pnpm install prisma tsx @types/pg --save-dev + pnpm install @prisma/client @prisma/adapter-pg dotenv pg pnpx tsc --init @@ -98,7 +100,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash yarn init -y - yarn add prisma typescript ts-node @types/node --save-dev + yarn add prisma tsx @types/pg --save-dev + yarn add @prisma/client @prisma/adapter-pg dotenv pg npx tsc --init @@ -108,7 +111,8 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash bun init -y - bun install prisma typescript ts-node @types/node --save-dev + bun install prisma tsx @types/pg --save-dev + bun install @prisma/client @prisma/adapter-pg dotenv pg bunx tsc --init @@ -168,15 +172,6 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t postgres://prisma.[PROJECT-REF]... ``` - In your schema.prisma file, edit your `datasource db` configs to reference your DIRECT_URL - ```text schema.prisma - datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") - } - ``` - @@ -190,9 +185,56 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t - + + + Add `import "dotenv/config"` to the generated `prisma.config.ts`. If you are using a serverless environment, change the data source URL to `DIRECT_URL`. + + + + + + + ```ts prisma.config.ts + import "dotenv/config"; + import { defineConfig, env } from "prisma/config"; + + export default defineConfig({ + schema: "prisma/schema", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, + }); + ``` + + + ```ts prisma.config.ts + import "dotenv/config"; + import { defineConfig, env } from "prisma/config"; + + export default defineConfig({ + schema: "prisma/schema", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DIRECT_URL"), + }, + }); + ``` + + + + + + + + + - If you have already modified your Supabase database, synchronize it with your migration file. Otherwise create new tables for your database + If you have already modified your Supabase database, synchronize it with your migration file. Otherwise create new tables for your database, then generate the Prisma client. @@ -232,25 +274,25 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npx prisma migrate dev --name first_prisma_migration - + npx prisma generate ``` ```bash pnpx prisma migrate dev --name first_prisma_migration - + pnpx prisma generate ``` ```bash npx prisma migrate dev --name first_prisma_migration - + npx prisma generate ``` ```bash bunx prisma migrate dev --name first_prisma_migration - + bunx prisma generate ``` @@ -291,6 +333,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npx prisma migrate resolve --applied 0_init_supabase + npx prisma generate ``` @@ -319,6 +362,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash pnpx prisma migrate resolve --applied 0_init_supabase + pnpx prisma generate ``` @@ -347,6 +391,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash npx prisma migrate resolve --applied 0_init_supabase + npx prisma generate ``` @@ -375,6 +420,7 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```bash bunx prisma migrate resolve --applied 0_init_supabase + bunx prisma generate ``` @@ -385,47 +431,6 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t - - - - Install the Prisma client and generate its model - - - - - - ```sh - npm install @prisma/client - npx prisma generate - ``` - - - ```sh - pnpm install @prisma/client - pnpx prisma generate - ``` - - - ```sh - yarn add @prisma/client - npx prisma generate - ``` - - - ```sh - bun install @prisma/client - bunx prisma generate - ``` - - - - @@ -433,13 +438,15 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t ```ts index.ts - const { PrismaClient } = require('@prisma/client'); + import "dotenv/config"; + import { PrismaClient } from "./generated/prisma/client"; + import { PrismaPg } from "@prisma/adapter-pg"; - const prisma = new PrismaClient(); + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); + export const prisma = new PrismaClient({ adapter }); async function main() { - //change to reference a table in your schema - const val = await prisma..findMany({ + const val = await prisma.user.findMany({ take: 10, }); console.log(val); @@ -452,10 +459,10 @@ If you plan to solely use Prisma instead of the Supabase Data API (PostgREST), t .catch(async (e) => { console.error(e); await prisma.$disconnect(); - process.exit(1); + process.exit(1); }); - ``` + diff --git a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx index fb0f3bd25112c..ff3757e0eab09 100644 --- a/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx +++ b/apps/docs/content/guides/database/prisma/prisma-troubleshooting.mdx @@ -142,18 +142,16 @@ A Prisma migration is referencing a schema it is not permitted to manage. ### Solutions: [#solutions-cross-schema-references] -- Multi-Schema support: If the external schema isn't Supabase managed, modify your `prisma.schema` file to enable the multi-Schema preview +- Multi-schema support: If the external schema isn't Supabase managed, list the relevant schemas on the `datasource` block in your `schema.prisma` file. -```ts prisma.schema +```prisma schema.prisma generator client { - provider = "prisma-client-js" - previewFeatures = ["multiSchema"] //Add line + provider = "prisma-client" + output = "../generated/prisma" } datasource db { provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") schemas = ["public", "other_schema"] //list out relevant schemas } ``` diff --git a/apps/docs/content/guides/functions.mdx b/apps/docs/content/guides/functions.mdx index fdcf0627f9ad4..718391265321a 100644 --- a/apps/docs/content/guides/functions.mdx +++ b/apps/docs/content/guides/functions.mdx @@ -52,156 +52,284 @@ Edge Functions are server-side TypeScript functions, distributed globally at the Check out the [Edge Function Examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions) in our GitHub repository.
- {[ - { - name: 'With supabase-js', - description: 'Use the Supabase client inside your Edge Function.', - href: '/guides/functions/auth', - }, - { - name: 'Type-Safe SQL with Kysely', - description: - 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.', - href: '/guides/functions/kysely-postgres', - }, - { - name: 'Monitoring with Sentry', - description: 'Monitor Edge Functions with the Sentry Deno SDK.', - href: '/guides/functions/examples/sentry-monitoring', - }, - { - name: 'With CORS headers', - description: 'Send CORS headers for invoking from the browser.', - href: '/guides/functions/cors', - }, - { - name: 'React Native with Stripe', - description: 'Full example for using Supabase and Stripe, with Expo.', - href: 'https://github.com/supabase-community/expo-stripe-payments-with-supabase-functions', - }, - { - name: 'Flutter with Stripe', - description: 'Full example for using Supabase and Stripe, with Flutter.', - href: 'https://github.com/supabase-community/flutter-stripe-payments-with-supabase-functions', - }, - { - name: 'Building a RESTful Service API', - description: - 'Learn how to use HTTP methods and paths to build a RESTful service for managing tasks.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/restful-tasks/index.ts', - }, - { - name: 'Working with Supabase Storage', - description: 'An example on reading a file from Supabase Storage.', - href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/read-storage/index.ts', - }, - { - name: 'Open Graph Image Generation', - description: 'Generate Open Graph images with Deno and Supabase Edge Functions.', - href: '/guides/functions/examples/og-image', - }, - { - name: 'OG Image Generation & Storage CDN Caching', - description: 'Cache generated images with Supabase Storage CDN.', - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/og-image-with-storage-cdn', - }, - { - name: 'Get User Location', - description: `Get user location data from user's IP address.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/location', - }, - { - name: 'Cloudflare Turnstile', - description: `Protecting Forms with Cloudflare Turnstile.`, - href: '/guides/functions/examples/cloudflare-turnstile', - }, - { - name: 'Connect to Postgres', - description: `Connecting to Postgres from Edge Functions.`, - href: '/guides/functions/connect-to-postgres', - }, - { - name: 'GitHub Actions', - description: `Deploying Edge Functions with GitHub Actions.`, - href: '/guides/functions/examples/github-actions', - }, - { - name: 'Oak Server Middleware', - description: `Request Routing with Oak server middleware.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/oak-server', - }, - { - name: 'Hugging Face', - description: `Access 100,000+ Machine Learning models.`, - href: '/guides/ai/examples/huggingface-image-captioning', - }, - { - name: 'Amazon Bedrock', - description: `Amazon Bedrock Image Generator`, - href: '/guides/functions/examples/amazon-bedrock-image-generator', - }, - { - name: 'OpenAI', - description: `Using OpenAI in Edge Functions.`, - href: '/guides/ai/examples/openai', - }, - { - name: 'Stripe Webhooks', - description: `Handling signed Stripe Webhooks with Edge Functions.`, - href: '/guides/functions/examples/stripe-webhooks', - }, - { - name: 'Send emails', - description: `Send emails in Edge Functions with Resend.`, - href: '/guides/functions/examples/send-emails', - }, - { - name: 'Web Stream', - description: `Server-Sent Events in Edge Functions.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/streams', - }, - { - name: 'Puppeteer', - description: `Generate screenshots with Puppeteer.`, - href: '/guides/functions/examples/screenshots', - }, - { - name: 'Discord Bot', - description: `Building a Slash Command Discord Bot with Edge Functions.`, - href: '/guides/functions/examples/discord-bot', - }, - { - name: 'Telegram Bot', - description: `Building a Telegram Bot with Edge Functions.`, - href: '/guides/functions/examples/telegram-bot', - }, - { - name: 'Upload File', - description: `Process multipart/form-data.`, - href: 'https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/file-upload-storage', - }, - { - name: 'Upstash Redis', - description: `Build an Edge Functions Counter with Upstash Redis.`, - href: '/guides/functions/examples/upstash-redis', - }, - { - name: 'Rate Limiting', - description: `Rate Limiting Edge Functions with Upstash Redis.`, - href: '/guides/functions/examples/rate-limiting', - }, - { - name: 'Slack Bot Mention Edge Function', - description: `Slack Bot handling Slack mentions in Edge Function`, - href: '/guides/functions/examples/slack-bot-mention', - }, - ].map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + Use the Supabase client inside your Edge Function. + + +
+
+ + + Combining Kysely with Deno Postgres gives you a convenient developer experience for + interacting directly with your Postgres database. + + +
+
+ + + Monitor Edge Functions with the Sentry Deno SDK. + + +
+
+ + + Send CORS headers for invoking from the browser. + + +
+
+ + + Full example for using Supabase and Stripe, with Expo. + + +
+
+ + + Full example for using Supabase and Stripe, with Flutter. + + +
+
+ + + Learn how to use HTTP methods and paths to build a RESTful service for managing tasks. + + +
+
+ + + An example on reading a file from Supabase Storage. + + +
+
+ + + Generate Open Graph images with Deno and Supabase Edge Functions. + + +
+
+ + + Cache generated images with Supabase Storage CDN. + + +
+
+ + + Get user location data from user's IP address. + + +
+
+ + + Protecting Forms with Cloudflare Turnstile. + + +
+
+ + + Connecting to Postgres from Edge Functions. + + +
+
+ + + Deploying Edge Functions with GitHub Actions. + + +
+
+ + + Request Routing with Oak server middleware. + + +
+
+ + + Access 100,000+ Machine Learning models. + + +
+
+ + + Amazon Bedrock Image Generator + + +
+
+ + + Using OpenAI in Edge Functions. + + +
+
+ + + Handling signed Stripe Webhooks with Edge Functions. + + +
+
+ + + Send emails in Edge Functions with Resend. + + +
+
+ + + Server-Sent Events in Edge Functions. + + +
+
+ + + Generate screenshots with Puppeteer. + + +
+
+ + + Building a Slash Command Discord Bot with Edge Functions. + + +
+
+ + + Building a Telegram Bot with Edge Functions. + + +
+
+ + + Process multipart/form-data. + + +
+
+ + + Build an Edge Functions Counter with Upstash Redis. + + +
+
+ + + Rate Limiting Edge Functions with Upstash Redis. + + +
+
+ + + Slack Bot handling Slack mentions in Edge Function + + +
diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index 4580764827206..7559d93015f22 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -9,43 +9,28 @@ hideToc: true
-
- {[ - { - title: 'Build with AI tools', - description: 'Develop with Supabase AI-first using plugins, MCP, and skills.', - hasLightIcon: true, - href: '/guides/ai', - }, - { - title: 'API Keys', - hasLightIcon: true, - href: '/guides/getting-started/api-keys', - description: 'Learn about the different API keys in Supabase and how to use them.', - }, - { - title: 'Local Development', - hasLightIcon: true, - href: '/guides/cli/getting-started', - description: 'Use the Supabase CLI to develop locally and collaborate between teams.', - } - ].map((resource) => { - return ( - - - {resource.description} - - - ) - -})} - -
+
+ + + Develop with Supabase AI-first using plugins, MCP, and skills. + + + + + Learn about the different API keys in Supabase and how to use them. + + + + + Use the Supabase CLI to develop locally and collaborate between teams. + + +
@@ -54,41 +39,45 @@ hideToc: true ### Use cases
- {[ - { - title: 'AI, Vectors, and embeddings', - href: '/guides/ai#examples', - description: `Build AI-enabled applications using our Vector toolkit.`, - icon: '/docs/img/icons/openai_logo', - hasLightIcon: true, - }, - { - title: 'Subscription Payments (SaaS)', - href: 'https://github.com/vercel/nextjs-subscription-payments#nextjs-subscription-payments-starter', - description: `Clone, deploy, and fully customize a SaaS subscription application with Next.js.`, - icon: '/docs/img/icons/nextjs-icon', - }, - { - title: 'Partner Gallery', - href: 'https://github.com/supabase-community/partner-gallery-example#supabase-partner-gallery-example', - description: `Postgres full-text search, image storage, and more.`, - icon: '/docs/img/icons/nextjs-icon', - }, - ].map((item) => { - return ( - - - {item.description} - - - ) - })} + + + Build AI-enabled applications using our Vector toolkit. + + + + + Clone, deploy, and fully customize a SaaS subscription application with Next.js. + + + + + Postgres full-text search, image storage, and more. + +
### Framework quickstarts @@ -96,130 +85,142 @@ hideToc: true <$Show if="docs:framework_quickstarts">
- {[ - { - title: 'React', - href: '/guides/getting-started/quickstarts/reactjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app.', - icon: '/docs/img/icons/react-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Next.js', - href: '/guides/getting-started/quickstarts/nextjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Next.js app.', - icon: '/docs/img/icons/nextjs-icon', - hasLightIcon: true, - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Nuxt', - href: '/guides/getting-started/quickstarts/nuxtjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Nuxt app.', - icon: '/docs/img/icons/nuxt-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Hono', - href: '/guides/getting-started/quickstarts/hono', - description: - 'Learn how to create a Supabase project, add some sample data to your database, secure it with auth, and query the data from a Hono app.', - icon: '/docs/img/icons/hono-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'RedwoodJS', - href: '/guides/getting-started/quickstarts/redwoodjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database using Prisma migration and seeds, and query the data from a RedwoodJS app.', - icon: '/docs/img/icons/redwood-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Flutter', - href: '/guides/getting-started/quickstarts/flutter', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app.', - icon: '/docs/img/icons/flutter-icon', - enabled: isFeatureEnabled('sdk:dart'), - }, - { - title: 'iOS SwiftUI', - href: '/guides/getting-started/quickstarts/ios-swiftui', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app.', - icon: '/docs/img/icons/swift-icon', - enabled: isFeatureEnabled('sdk:swift'), - }, - { - title: 'Android Kotlin', - href: '/guides/getting-started/quickstarts/kotlin', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from an Android Kotlin app.', - icon: '/docs/img/icons/kotlin-icon', - enabled: isFeatureEnabled('sdk:kotlin'), - }, - { - title: 'SvelteKit', - href: '/guides/getting-started/quickstarts/sveltekit', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SvelteKit app.', - icon: '/docs/img/icons/svelte-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'SolidJS', - href: '/guides/getting-started/quickstarts/solidjs', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app.', - icon: '/docs/img/icons/solidjs-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Vue', - href: '/guides/getting-started/quickstarts/vue', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app.', - icon: '/docs/img/icons/vuejs-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'TanStack Start', - href: '/guides/getting-started/quickstarts/tanstack', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a TanStack Start app.', - icon: '/docs/img/icons/tanstack-icon', - hasLightIcon: true, - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - { - title: 'Refine', - href: '/guides/getting-started/quickstarts/refine', - description: - 'Learn how to create a Supabase project, add some sample data to your database, and query the data from a Refine app.', - icon: '/docs/img/icons/refine-icon', - enabled: isFeatureEnabled('docs:framework_quickstarts'), - }, - ] - .filter((item) => item.enabled !== false) - .map((item) => { - return ( - - - {item.description} - - - ) - })} + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a React app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Next.js app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Nuxt app. + + + + + Learn how to create a Supabase project, add some sample data to your database, secure it with + auth, and query the data from a Hono app. + + + + + Learn how to create a Supabase project, add some sample data to your database using Prisma + migration and seeds, and query the data from a RedwoodJS app. + + + <$Show if="sdk:dart"> + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Flutter app. + + + + <$Show if="sdk:swift"> + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from an iOS app. + + + + <$Show if="sdk:kotlin"> + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from an Android Kotlin app. + + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a SvelteKit app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a SolidJS app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Vue app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a TanStack Start app. + + + + + Learn how to create a Supabase project, add some sample data to your database, and query the + data from a Refine app. + +
@@ -235,90 +236,97 @@ hideToc: true ### Web app demos
- { - [ - { - title: 'Next.js', - href: '/guides/getting-started/tutorials/with-nextjs', - description: - 'Learn how to build a user management app with Next.js and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/nextjs-icon', - hasLightIcon: true, - }, - { - title: 'React', - href: '/guides/getting-started/tutorials/with-react', - description: - 'Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/react-icon', - }, - { - title: 'Vue 3', - href: '/guides/getting-started/tutorials/with-vue-3', - description: - 'Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/vuejs-icon', - }, - { - title: 'Nuxt 3', - href: '/guides/getting-started/tutorials/with-nuxt-3', - description: - 'Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/nuxt-icon', - }, - { - title: 'Angular', - href: '/guides/getting-started/tutorials/with-angular', - description: - 'Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/angular-icon', - }, - { - title: 'RedwoodJS', - href: '/guides/getting-started/tutorials/with-redwoodjs', - description: - 'Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/redwood-icon', - }, - { - title: 'Svelte', - href: '/guides/getting-started/tutorials/with-svelte', - description: - 'Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/svelte-icon', - }, - { - title: 'SvelteKit', - href: '/guides/getting-started/tutorials/with-sveltekit', - description: - 'Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/svelte-icon', - }, - { - title: 'Refine', - href: '/guides/getting-started/tutorials/with-refine', - description: - 'Learn how to build a user management app with Refine and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/refine-icon', - } - ] -.map((item) => { - return ( - - - {item.description} - - - ) - -})} - + + + Learn how to build a user management app with Next.js and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Refine and Supabase Database, Auth, and Storage functionality. + +
@@ -327,90 +335,105 @@ hideToc: true ### Mobile tutorials
- {[ - { - title: 'Flutter', - href: '/guides/getting-started/tutorials/with-flutter', - description: - 'Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/flutter-icon', - enabled: isFeatureEnabled('sdk:dart') - }, - { - title: 'Expo React Native', - href: '/guides/getting-started/tutorials/with-expo-react-native', - description: - 'Learn how to build a user management app with Expo React Native and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/expo-icon', - hasLightIcon: true, - enabled: true - }, - { - title: 'Expo React Native Social Auth', - href: '/guides/getting-started/tutorials/with-expo-react-native-social-auth', - description: - 'Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality.', - icon: '/docs/img/icons/expo-icon', - hasLightIcon: true - }, - { - title: 'Android Kotlin', - href: '/guides/getting-started/tutorials/with-kotlin', - description: - 'Learn how to build a product management app with Android and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/kotlin-icon', - enabled: isFeatureEnabled('sdk:kotlin') - }, - { - title: 'iOS Swift', - href: '/guides/getting-started/tutorials/with-swift', - description: - 'Learn how to build a user management app with iOS and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/swift-icon', - enabled: isFeatureEnabled('sdk:swift') - }, - { - title: 'Ionic React', - href: '/guides/getting-started/tutorials/with-ionic-react', - description: - 'Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon', - enabled: true - }, - { - title: 'Ionic Vue', - href: '/guides/getting-started/tutorials/with-ionic-vue', - description: - 'Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon', - enabled: true - }, - { - title: 'Ionic Angular', - href: '/guides/getting-started/tutorials/with-ionic-angular', - description: - 'Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality.', - icon: '/docs/img/icons/ionic-icon', - enabled: true - } - ] -.filter((item) => item.enabled !== false) -.map((item) => { - return ( - - - {item.description} - - - ) - -})} - + <$Show if="sdk:dart"> + + + Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality. + + + + + + Learn how to build a user management app with Expo React Native and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to implement social authentication in an app with Expo React Native and Supabase Database and Auth functionality. + + + <$Show if="sdk:kotlin"> + + + Learn how to build a product management app with Android and Supabase Database, Auth, and Storage functionality. + + + + <$Show if="sdk:swift"> + + + Learn how to build a user management app with iOS and Supabase Database, Auth, and Storage functionality. + + + + + + Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality. + + + + + Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality. + +
diff --git a/apps/docs/content/guides/platform/temporary-access.mdx b/apps/docs/content/guides/platform/temporary-access.mdx new file mode 100644 index 0000000000000..32e067c380c16 --- /dev/null +++ b/apps/docs/content/guides/platform/temporary-access.mdx @@ -0,0 +1,118 @@ +--- +title: 'Temporary access' +description: 'Enable temporary access for short-lived database connections tied to a Supabase user.' +--- + +Your Supabase project supports connecting to the Postgres database using either your Supabase API token (Personal Access Token) or your current dashboard session token (JWT). This is called temporary access, as the authentication tokens can be short-lived and tied directly to a specific Supabase user. Temporary access is disabled by default. + +Enabling temporary access only applies to connections to Postgres and Supavisor ("Connection Pooler"); all HTTP APIs offered by Supabase (e.g., PostgREST, Storage, Auth) require authentication tokens specific to the service and are independent of the Supabase platform user(s). + + + +Projects need to be at least on Postgres 17.6.1.081 (or higher) to enable temporary access. You can find the Postgres version of your project on the [infrastructure settings](/dashboard/project/_/settings/infrastructure) page. If your project is on an older version, you will need to [upgrade](/docs/guides/platform/upgrading) to use this feature. + + + +## Manage temporary access via the dashboard + +The easiest way to manage temporary access is via the "Enable temporary access" settings section in [Database Settings page](/dashboard/project/_/database/settings) of the dashboard. + +## Manage temporary access via the Management API + +You can also manage temporary access using the Management API: + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_MANAGEMENT_API_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Get current temporary access status +curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" + +# Enable temporary access +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "state":"enabled" + }' + +# Disable temporary access +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "state":"disabled" + }' +``` + +## Configure user access + +Once temporary access has been enabled, project users must be authorized and mapped to Postgres roles they are allowed to access. Each user can be authorized to "assume" one or more Postgres roles using temporary access. + +When a user is authorized to assume a Postgres role, the user's access token (Personal Access Token (PAT) or Scoped PAT) will be used as the password for the Postgres role. + + + +Postgres roles can still be accessed using the password configured on the role. This allows long-lived service connections to continue using the existing credentials. + +Temporary access authentication only applies for Supabase project users authenticating to the database, and assuming the given Postgres role. No new roles or users are created in the Postgres database and the assumed role's permissions will still apply. + + + +### Apply temporary access restrictions + +A user's temporary access can also be restricted to a validity period, after which their temporary access will expire and even though the access token is still valid, the database will reject the connection. + +IP address restrictions can also be applied, ensuring that temporary access will only be authorized from allowed network ranges (IPv4 and/or IPv6). + +#### Applying restrictions with the management API + +Restrictions can also be applied through the Management API. + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_MANAGEMENT_API_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Restrict temporary access to IPv4 ranges and expiry date +# user_id is the gotrue_id of the user with access to the project +curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit" \ + -H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "00000000-1111-2222-3333-444444444444", + "user_roles": [ + { + "role": "postgres", + "allowed_networks": { + "allowed_cidrs": [{ "cidr": "176.1.12.1/32" }] + }, + "expires_at": 1758721065775 + } + ] + }' +``` + +## Using temporary access + +To log in to the database using temporary access, existing connection strings can be used and only the password needs to be changed to the user's API or dashboard token. + +For example, if a user has been authorized to assume the `postgres` role: + +``` +psql 'postgres://postgres:sbp_111222333aaabbbccc@db.{project-ref}.supabase.co/postgres' +``` + +Since Supabase API tokens can be used, it is also possible to generate API tokens for services you don't want to share your Postgres role password with (for example a GitHub Action). The API token can be configured with an expiry time and temporary access-specific restrictions can also be applied. + +Connecting via the shared connection pooler requires the addition of a new connection option. This can be applied either directly in the connection URI or as `conninfo` (easier to read): + +``` +# directly in the URI +psql 'postgres://postgres.{project-ref}:sbp_111222333aaabbbccc@aws-1-us-west-1.pooler.supabase.com:5432/postgres?options=-c%20jit%3don' + +# or as a connection info string +psql "host=aws-1-us-west-1.pooler.supabase.com user=postgres.{project-ref} options='-c jit=on'" +``` diff --git a/apps/docs/content/guides/realtime.mdx b/apps/docs/content/guides/realtime.mdx index 9a4331929cb1f..cd79fccadff5d 100644 --- a/apps/docs/content/guides/realtime.mdx +++ b/apps/docs/content/guides/realtime.mdx @@ -25,35 +25,35 @@ Check the [Getting Started](/docs/guides/realtime/getting_started) guide to get ## Examples
- {[ - { - name: 'Multiplayer.dev', - description: 'Showcase application displaying cursor movements and chat messages using Broadcast.', - href: 'https://multiplayer.dev', - }, - { - name: 'Chat', - description: 'Supabase UI chat component using Broadcast to send message between users.', - href: 'https://supabase.com/ui/docs/nextjs/realtime-chat' - }, - { - name: 'Avatar Stack', - description: 'Supabase UI avatar stack component using Presence to track connected users.', - href: 'https://supabase.com/ui/docs/nextjs/realtime-avatar-stack' - }, - { - name: 'Realtime Cursor', - description: "Supabase UI realtime cursor component using Broadcast to share users' cursors to build collaborative applications.", - href: 'https://supabase.com/ui/docs/nextjs/realtime-cursor' - } -].map((x) => ( -
- - {x.description} +
+ + + Showcase application displaying cursor movements and chat messages using Broadcast. + + +
+
+ + + Supabase UI chat component using Broadcast to send message between users. + + +
+
+ + + Supabase UI avatar stack component using Presence to track connected users. + + +
+
+ + + Supabase UI realtime cursor component using Broadcast to share users' cursors to build + collaborative applications. +
-))} -
## Resources @@ -61,22 +61,19 @@ Check the [Getting Started](/docs/guides/realtime/getting_started) guide to get Find the source code and documentation in the Supabase GitHub repository.
- {[ - { - name: 'Supabase Realtime', - description: 'View the source code.', - href: 'https://github.com/supabase/realtime', - }, - { - name: 'Realtime: Multiplayer Edition', - description: 'Read more about Supabase Realtime.', - href: 'https://supabase.com/blog/supabase-realtime-multiplayer-general-availability', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + View the source code. + +
+
+ + + Read more about Supabase Realtime. + + +
diff --git a/apps/docs/content/guides/resources.mdx b/apps/docs/content/guides/resources.mdx index 609505dfb8192..3ebd7d7c6f835 100644 --- a/apps/docs/content/guides/resources.mdx +++ b/apps/docs/content/guides/resources.mdx @@ -10,39 +10,18 @@ hideToc: true
-
- { - [ - { - title: 'Examples', - hasLightIcon: true, - href: '/guides/resources/examples', - description: 'Official GitHub examples, curated content from the community, and more.', - }, - { - title: 'Glossary', - hasLightIcon: true, - href: '/guides/resources/glossary', - description: 'Definitions for terminology and acronyms used in the Supabase documentation.', - } - ] -.map((resource) => { - return ( - - - {resource.description} - - - ) - -})} - -
+
+ + + Official GitHub examples, curated content from the community, and more. + + + + + Definitions for terminology and acronyms used in the Supabase documentation. + + +
@@ -53,88 +32,159 @@ hideToc: true
-
- { - [ - { - title: 'Auth0', - icon: '/docs/img/icons/auth0-icon', - href: '/guides/resources/migrating-to-supabase/auth0', - description: 'Move your auth users from Auth0 to a Supabase project.', - hasLightIcon: true, - }, - { - title: 'Firebase Auth', - icon: '/docs/img/icons/firebase-icon', - href: '/guides/resources/migrating-to-supabase/firebase-auth', - description: 'Move your auth users from a Firebase project to a Supabase project.', - }, - { - title: 'Firestore Data', - icon: '/docs/img/icons/firebase-icon', - href: '/guides/resources/migrating-to-supabase/firestore-data', - description: 'Migrate the contents of a Firestore collection to a single Postgres table.', - }, - { - title: 'Firebase Storage', - icon: '/docs/img/icons/firebase-icon', - href: '/guides/resources/migrating-to-supabase/firebase-storage', - description: 'Convert your Firebase Storage files to Supabase Storage.' - }, - { - title: 'Heroku', - icon: '/docs/img/icons/heroku-icon', - href: '/guides/resources/migrating-to-supabase/heroku', - description: 'Migrate your Heroku Postgres database to Supabase.' - }, - { - title: 'Render', - icon: '/docs/img/icons/render-icon', - href: '/guides/resources/migrating-to-supabase/render', - description: 'Migrate your Render Postgres database to Supabase.' - }, - { - title: 'Amazon RDS', - icon: '/docs/img/icons/aws-rds-icon', - href: '/guides/resources/migrating-to-supabase/amazon-rds', - description: 'Migrate your Amazon RDS database to Supabase.' - }, - { - title: 'Postgres', - icon: '/docs/img/icons/postgres-icon', - href: '/guides/resources/migrating-to-supabase/postgres', - description: 'Migrate your Postgres database to Supabase.' - }, - { - title: 'MySQL', - icon: '/docs/img/icons/mysql-icon', - href: '/guides/resources/migrating-to-supabase/mysql', - description: 'Migrate your MySQL database to Supabase.' - }, - { - title: 'Microsoft SQL Server', - icon: '/docs/img/icons/mssql-icon', - href: '/guides/resources/migrating-to-supabase/mssql', - description: 'Migrate your Microsoft SQL Server database to Supabase.' - } - ] -.map((product) => { - return ( - - - {product.description} - - - ) - -})} - -
+
+ + + Move your auth users from Auth0 to a Supabase project. + + + + + Move your auth users from a Firebase project to a Supabase project. + + + + + Migrate the contents of a Firestore collection to a single Postgres table. + + + + + Convert your Firebase Storage files to Supabase Storage. + + + + + Migrate your Heroku Postgres database to Supabase. + + + + + Migrate your Render Postgres database to Supabase. + + + + + Migrate your Amazon RDS database to Supabase. + + + + + Migrate your Postgres database to Supabase. + + + + + Migrate your MySQL database to Supabase. + + + + + Migrate your Microsoft SQL Server database to Supabase. + + +
@@ -145,57 +195,64 @@ hideToc: true -
- { - [ - { - title: 'Managing Indexes', - hasLightIcon: true, - href: '/guides/database/postgres/indexes', - description: 'Improve query performance using various index types in Postgres.' - }, - { - title: 'Cascade Deletes', - hasLightIcon: true, - href: '/guides/database/postgres/cascade-deletes', - description: 'Understand the types of foreign key constraint deletes.' - }, - { - title: 'Drop all tables in schema', - hasLightIcon: true, - href: '/guides/database/postgres/dropping-all-tables-in-schema', - description: 'Delete all tables in a given schema.' - }, - { - title: 'Select first row per group', - hasLightIcon: true, - href: '/guides/database/postgres/first-row-in-group', - description: 'Retrieve the first row in each distinct group.' - }, - { - title: 'Print Postgres version', - hasLightIcon: true, - href: '/guides/database/postgres/which-version-of-postgres', - description: 'Find out which version of Postgres you are running.' - } - ] -.map((resource) => { - return ( - - - {resource.description} - - - ) - -})} - -
+
+ + + Improve query performance using various index types in Postgres. + + + + + Understand the types of foreign key constraint deletes. + + + + + Delete all tables in a given schema. + + + + + Retrieve the first row in each distinct group. + + + + + Find out which version of Postgres you are running. + + +
diff --git a/apps/docs/content/guides/self-hosting.mdx b/apps/docs/content/guides/self-hosting.mdx index 9f16ea9ba9994..f5923055c7bbb 100644 --- a/apps/docs/content/guides/self-hosting.mdx +++ b/apps/docs/content/guides/self-hosting.mdx @@ -34,23 +34,20 @@ The fastest and recommended way to self-host Supabase is to use Docker. There are several other options to deploy Supabase. If you're interested in helping these projects, visit our [Community](/contribute) page.
- {selfHostingCommunity.map((x) => ( -
- - - {x.name} - - } - > - {x.description} - - -
- -))} - +
+ + Kubernetes}> + Helm charts to deploy a Supabase on Kubernetes. + + +
+
+ + Traefik}> + A self-hosted Supabase setup with Traefik as a reverse proxy. + + +
## About self-hosting diff --git a/apps/docs/content/guides/storage.mdx b/apps/docs/content/guides/storage.mdx index 51b8a466b64b0..81643f11439c4 100644 --- a/apps/docs/content/guides/storage.mdx +++ b/apps/docs/content/guides/storage.mdx @@ -71,15 +71,20 @@ Specialized storage for vector embeddings and similarity search operations. Desi Check out all of the Storage [templates and examples](https://github.com/supabase/supabase/tree/master/examples/storage) in our GitHub repository.
- {storageExamples.map((x) => ( -
- - - {x.description} - - -
- ))} +
+ + + Use Uppy to upload files to Supabase Storage using the TUS protocol (resumable uploads). + + +
## Resources @@ -87,22 +92,16 @@ Check out all of the Storage [templates and examples](https://github.com/supabas Find the source code and documentation in the Supabase GitHub repository.
- {[ - { - name: 'Supabase Storage API', - description: 'View the source code.', - href: 'https://github.com/supabase/storage-api', - }, - { - name: 'OpenAPI Spec', - description: 'See the Swagger Documentation for Supabase Storage.', - href: 'https://supabase.github.io/storage/', - }, - ].map((x) => ( -
- - {x.description} - -
- ))} +
+ + View the source code. + +
+
+ + + See the Swagger Documentation for Supabase Storage. + + +
diff --git a/apps/docs/features/docs/MdxBase.tsx b/apps/docs/features/docs/MdxBase.tsx index 0123fb24108ca..d356112565559 100644 --- a/apps/docs/features/docs/MdxBase.tsx +++ b/apps/docs/features/docs/MdxBase.tsx @@ -1,6 +1,5 @@ import { preprocessMdxWithDefaults } from '~/features/directives/utils' import { components } from '~/features/docs/MdxBase.shared' -import { guidesData } from '~/lib/guidesData' import { SerializeOptions } from '~/types/next-mdx-remote-serialize' import { isFeatureEnabled } from 'common' import { MDXRemote } from 'next-mdx-remote-client/rsc' @@ -39,7 +38,7 @@ const MDXRemoteBase = async ({ } = mdxOptions const finalOptions = { - scope: { isFeatureEnabled, ...guidesData }, + scope: { isFeatureEnabled }, ...mdxOptions, ...otherOptions, mdxOptions: { diff --git a/apps/docs/features/docs/Reference.apiPage.tsx b/apps/docs/features/docs/Reference.apiPage.tsx index e1bd542dc98dd..3e77558f3e6a0 100644 --- a/apps/docs/features/docs/Reference.apiPage.tsx +++ b/apps/docs/features/docs/Reference.apiPage.tsx @@ -9,7 +9,7 @@ import { SidebarSkeleton } from '~/layouts/MainSkeleton' export async function ApiReferencePage() { return ( - + + diff --git a/apps/docs/features/docs/Reference.navigation.client.tsx b/apps/docs/features/docs/Reference.navigation.client.tsx index 3e991fd6dc1c7..75e2ea65fb2c1 100644 --- a/apps/docs/features/docs/Reference.navigation.client.tsx +++ b/apps/docs/features/docs/Reference.navigation.client.tsx @@ -6,6 +6,7 @@ import { BASE_PATH } from '~/lib/constants' import { debounce } from 'lodash-es' import { ChevronUp } from 'lucide-react' import Link from 'next/link' +import { usePathname } from 'next/navigation' import { Collapsible } from 'radix-ui' import type { HTMLAttributes, MouseEvent, PropsWithChildren } from 'react' import { @@ -36,7 +37,6 @@ function subscribeToPathname(callback: () => void) { if (patchCount === 0) { window.addEventListener('popstate', notifyPathnameListeners) - window.addEventListener('hashchange', notifyPathnameListeners) originalPushState = history.pushState.bind(history) history.pushState = (...args) => { @@ -58,7 +58,6 @@ function subscribeToPathname(callback: () => void) { if (patchCount === 0) { window.removeEventListener('popstate', notifyPathnameListeners) - window.removeEventListener('hashchange', notifyPathnameListeners) history.pushState = originalPushState! history.replaceState = originalReplaceState! originalPushState = null @@ -67,29 +66,40 @@ function subscribeToPathname(callback: () => void) { } } -function getLocation() { +function getPathname() { if (typeof window === 'undefined') return '' const pathname = window.location.pathname - const strippedPathname = pathname.startsWith(BASE_PATH) - ? pathname.slice(BASE_PATH.length) - : pathname - return `${strippedPathname}${window.location.hash}` + return pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname } -function getServerLocation() { +function getServerPathname() { return '' } -function useCurrentLocation() { - return useSyncExternalStore(subscribeToPathname, getLocation, getServerLocation) +function useCurrentPathname() { + return useSyncExternalStore(subscribeToPathname, getPathname, getServerPathname) } -export function ReferenceContentScrollHandler({ children }: PropsWithChildren) { +export function ReferenceContentScrollHandler({ + libPath, + version, + isLatestVersion, + children, +}: PropsWithChildren<{ + libPath: string + version: string + isLatestVersion: boolean +}>) { const [initiallyScrolled, setInitiallyScrolled] = useState(false) + const pathname = usePathname() + useEffect(() => { if (!initiallyScrolled) { - const initialSelectedSection = window.location.hash.replace(/^#/, '') + const initialSelectedSection = pathname.replace( + `/reference/${libPath}/${isLatestVersion ? '' : `${version}/`}`, + '' + ) if (initialSelectedSection) { const section = document.getElementById(initialSelectedSection) if (section) { @@ -100,7 +110,7 @@ export function ReferenceContentScrollHandler({ children }: PropsWithChildren) { setInitiallyScrolled(true) } - }, [initiallyScrolled]) + }, [pathname, libPath, version, isLatestVersion, initiallyScrolled]) return ( @@ -167,7 +177,7 @@ export function ReferenceNavigationScrollHandler({ } function deriveHref(basePath: string, section: AbbrevApiReferenceSection) { - return 'slug' in section ? `${basePath}#${section.slug}` : '' + return 'slug' in section ? `${basePath}/${section.slug}` : '' } function getLinkStyles(isActive: boolean, className?: string) { @@ -237,10 +247,10 @@ export function RefLink({ }) { const ref = useRef(null) - const location = useCurrentLocation() + const pathname = useCurrentPathname() const href = deriveHref(basePath, section) const isActive = - location === href || (location === basePath && href.replace(basePath, '') === '#introduction') + pathname === href || (pathname === basePath && href.replace(basePath, '') === '/introduction') useEffect(() => { if (ref.current) { @@ -283,15 +293,15 @@ export function RefLink({ function useCompoundRefLinkActive(basePath: string, section: AbbrevApiReferenceSection) { const [open, _setOpen] = useState(false) - const location = useCurrentLocation() + const pathname = useCurrentPathname() const parentHref = deriveHref(basePath, section) - const isParentActive = location === parentHref + const isParentActive = pathname === parentHref const childHrefs = useMemo( () => new Set((section.items || []).map((item) => deriveHref(basePath, item))), [basePath, section] ) - const isChildActive = childHrefs.has(location) + const isChildActive = childHrefs.has(pathname) const isActive = isParentActive || isChildActive diff --git a/apps/docs/features/docs/Reference.sdkPage.tsx b/apps/docs/features/docs/Reference.sdkPage.tsx index 5d095d520345c..9f03d453bde2c 100644 --- a/apps/docs/features/docs/Reference.sdkPage.tsx +++ b/apps/docs/features/docs/Reference.sdkPage.tsx @@ -21,7 +21,11 @@ export async function ClientSdkReferencePage({ sdkId, libVersion }: ClientSdkRef const menuData = NavItems[libraryMeta.meta[libVersion].libId] return ( - + {subcommandDetails.title} diff --git a/apps/docs/features/docs/Reference.selfHostingPage.tsx b/apps/docs/features/docs/Reference.selfHostingPage.tsx index 7fad5fefc0f0c..53773af35149a 100644 --- a/apps/docs/features/docs/Reference.selfHostingPage.tsx +++ b/apps/docs/features/docs/Reference.selfHostingPage.tsx @@ -48,7 +48,7 @@ export async function SelfHostingReferencePage({ const name = REFERENCES[servicePath.replaceAll('-', '_')].name return ( - + ...}` forms. + * For JSX titles, strips inline tags and collapses whitespace so the text + * content remains. */ -function convertResourceLists(content: string): string { - return content.replace(/\{\s*\[\s*\{[\s\S]*?\},?\s*\][\s\S]*?\}\)\}/g, (block) => { - const arrMatch = block.match(/\[([\s\S]+?)\]\s*\.(?:filter|map)\b/) - if (!arrMatch) return block - - // Collect top-level { ... } object literals from the array body. - const arr = arrMatch[1] - const objs: string[] = [] +function extractTitleAttr(attrs: string): string | null { + const strMatch = attrs.match(/\btitle=(?:"([^"]+)"|'([^']+)')/) + if (strMatch) return strMatch[1] ?? strMatch[2] + + const exprIdx = attrs.indexOf('title={') + if (exprIdx === -1) return null + + let i = exprIdx + 'title={'.length + const inner: string[] = [] + let depth = 1 + while (i < attrs.length && depth > 0) { + const ch = attrs[i] + if (ch === '{') depth++ + else if (ch === '}') { + depth-- + if (depth === 0) break + } + inner.push(ch) + i++ + } + return inner + .join('') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim() +} + +/** + * Converts `desc` + * blocks into markdown bullet lines: `- [title](href). description`. Without + * this, the rendered output keeps prop syntax (className, passHref, etc.) since + * stripping JSX tags discards inner text from props but leaves panel children + * floating without context. Applied to all guides so resource-card grids render + * consistently in the markdown view. + */ +function convertLinkPanels(content: string): string { + return content.replace(/([\s\S]*?)<\/Link>/g, (full, linkAttrs, body) => { + const hrefMatch = linkAttrs.match(/\bhref="([^"]+)"/) + if (!hrefMatch) return full + const href = hrefMatch[1] + + const panelOpen = body.match(/<(GlassPanel|IconPanel)\b/) + if (!panelOpen) return full + const panelName = panelOpen[1] + const panelStart = panelOpen.index ?? 0 + + // Walk past the opening tag, respecting JSX expression braces so attribute + // values like `title={...}` don't terminate parsing early. + let i = panelStart + panelOpen[0].length let depth = 0 - let start = -1 - for (let i = 0; i < arr.length; i++) { - if (arr[i] === '{') { - if (depth === 0) start = i - depth++ - } else if (arr[i] === '}' && --depth === 0 && start !== -1) { - objs.push(arr.slice(start, i + 1)) - start = -1 - } + while (i < body.length) { + const ch = body[i] + if (ch === '{') depth++ + else if (ch === '}') depth-- + else if (ch === '>' && depth === 0) break + i++ } + if (i >= body.length) return full + + const panelAttrs = body.slice(panelStart + panelOpen[0].length, i) + const closingTag = `` + const closeIdx = body.indexOf(closingTag, i) + if (closeIdx === -1) return full + + const title = extractTitleAttr(panelAttrs) + if (!title) return full + + const description = body + .slice(i + 1, closeIdx) + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim() - return objs - .map((o) => { - const title = o.match(/title:\s*['"`]([^'"`]+)['"`]/)?.[1] - const href = o.match(/href:\s*['"`]([^'"`]+)['"`]/)?.[1] - const desc = o.match(/description:\s*[`'"]([^`'"]+)[`'"]/)?.[1] - return title && href ? `- [${title}](${href})${desc ? `. ${desc}` : ''}` : '' - }) - .filter(Boolean) - .join('\n') + return `- [${title}](${href})${description ? `. ${description}` : ''}` }) } @@ -165,11 +210,8 @@ async function generate() { const withPartials = await inlinePartials(rawContent) const withSteps = convertStepHike(withPartials) - const withLists = - filePath === 'content/guides/getting-started.mdx' - ? convertResourceLists(withSteps) - : withSteps - const processed = stripJsxTags(withLists) + const withLinks = convertLinkPanels(withSteps) + const processed = stripJsxTags(withLinks) const header = [ data.title ? `# ${data.title}` : '', diff --git a/apps/docs/lib/guidesData.ts b/apps/docs/lib/guidesData.ts deleted file mode 100644 index 2bf5c7ccc37db..0000000000000 --- a/apps/docs/lib/guidesData.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Data used in guide MDX files. - * - * This data is passed to MDX files via scope to avoid using `export const` - * within MDX content, which is not supported by next-mdx-remote. - * - * @see https://github.com/hashicorp/next-mdx-remote#import--export - */ - -export const guidesData = { - // apps/docs/content/guides/self-hosting.mdx - selfHostingCommunity: [ - { - name: 'Kubernetes', - description: 'Helm charts to deploy a Supabase on Kubernetes.', - href: 'https://github.com/supabase-community/supabase-kubernetes', - }, - { - name: 'Traefik', - description: 'A self-hosted Supabase setup with Traefik as a reverse proxy.', - href: 'https://github.com/supabase-community/supabase-traefik', - }, - ], - - // apps/docs/content/guides/ai.mdx - aiExamples: [ - { - name: 'Headless Vector Search', - description: - 'A toolkit to perform vector similarity search on your knowledge base embeddings.', - href: '/guides/ai/examples/headless-vector-search', - }, - { - name: 'Image Search with OpenAI CLIP', - description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.', - href: '/guides/ai/examples/image-search-openai-clip', - }, - { - name: 'Hugging Face inference', - description: 'Generate image captions using Hugging Face.', - href: '/guides/ai/examples/huggingface-image-captioning', - }, - { - name: 'OpenAI completions', - description: 'Generate GPT text completions using OpenAI in Edge Functions.', - href: '/guides/ai/examples/openai', - }, - { - name: 'Building ChatGPT Plugins', - description: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.', - href: '/guides/ai/examples/building-chatgpt-plugins', - }, - { - name: 'Vector search with Next.js and OpenAI', - description: - 'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.', - href: '/guides/ai/examples/nextjs-vector-search', - }, - ], - - aiIntegrations: [ - { - name: 'OpenAI', - description: - 'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.', - href: '/guides/ai/examples/building-chatgpt-plugins', - }, - { - name: 'Amazon Bedrock', - description: - 'A fully managed service that offers a choice of high-performing foundation models from leading AI companies.', - href: '/guides/ai/integrations/amazon-bedrock', - }, - { - name: 'Hugging Face', - description: - "Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.", - href: '/guides/ai/hugging-face', - }, - { - name: 'LangChain', - description: - 'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.', - href: '/guides/ai/langchain', - }, - { - name: 'LlamaIndex', - description: 'LlamaIndex is a data framework for your LLM applications.', - href: '/guides/ai/integrations/llamaindex', - }, - ], - - // apps/docs/content/guides/storage.mdx - storageExamples: [ - { - name: 'Resumable Uploads with Uppy', - description: - 'Use Uppy to upload files to Supabase Storage using the TUS protocol (resumable uploads).', - href: 'https://github.com/supabase/supabase/tree/master/examples/storage/resumable-upload-uppy', - }, - ], -} diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index bfeb5206c222e..9243af773b278 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -171,20 +171,6 @@ const nextConfig = { source: '/guides/database/replication/replication-faq', destination: '/guides/database/replication/external-replication-faq', permanent: true, - }, - // Reference pages use hash anchors for sections; redirect the legacy - // path-style /reference//introduction (and versioned variants) - // back to the base reference URL. Order matters: introduction first so - // it strips to a bare URL, then the section rules add a hash anchor. - { - source: '/reference/:lib/:version(v\\d+)/:section', - destination: '/reference/:lib/:version#:section', - permanent: true, - }, - { - source: '/reference/:lib/:section((?!v\\d+$)[^/]+)', - destination: '/reference/:lib#:section', - permanent: true, }, ] }, diff --git a/apps/docs/resources/guide/guideModelLoader.ts b/apps/docs/resources/guide/guideModelLoader.ts index 5af209f9f3824..551bd98abfeaa 100644 --- a/apps/docs/resources/guide/guideModelLoader.ts +++ b/apps/docs/resources/guide/guideModelLoader.ts @@ -140,9 +140,9 @@ export class GuideModelLoader { }, (error) => { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { - return new FileNotFoundError('', error) + throw new FileNotFoundError('', error) } - return new Error( + throw new Error( `Failed to load guide from ${relPath}: ${extractMessageFromAnyError(error)}`, { cause: error, diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx index b4a45644668ac..54026ead90bca 100644 --- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -3,7 +3,6 @@ import { PropsWithChildren, useEffect } from 'react' import { OrganizationResourceBanner } from '../Organization/HeaderBanner' import { ClockSkewBanner } from '@/components/layouts/AppLayout/ClockSkewBanner' -import { FlyDeprecationBanner } from '@/components/layouts/AppLayout/FlyDeprecationBanner' import { NoticeBanner } from '@/components/layouts/AppLayout/NoticeBanner' import { StatusPageBanner } from '@/components/layouts/AppLayout/StatusPageBanner' import { BannerTOSUpdate } from '@/components/ui/BannerStack/Banners/BannerTOSUpdate' @@ -43,7 +42,6 @@ export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
{showNoticeBanner && } - {clockSkewBanner && }
diff --git a/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx b/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx index 1542c9658e138..a094881fb6231 100644 --- a/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx +++ b/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.tsx @@ -26,11 +26,13 @@ export function ErrorMatcher({ title, error, supportFormParams, className }: Err supportFormParams={supportFormParams} className={className} onRender={() => { - track('dashboard_error_created', { - source: 'error_display', - errorType: mapping?.id, - hasTroubleshooting: !!mapping, - }) + if (Math.random() < 0.1) { + track('dashboard_error_created', { + source: 'error_display', + errorType: mapping?.id, + hasTroubleshooting: !!mapping, + }) + } if (mapping) { track('inline_error_troubleshooter_exposed', { errorType: mapping.id }) } diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx index 6daf72b1047df..a4d46551f1ab3 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react' - -import { Markdown } from '@/components/interfaces/Markdown' +import { Markdown } from 'ui-patterns/Markdown' interface MarkdownContentProps { content: string | null | undefined @@ -34,5 +33,5 @@ export const MarkdownContent = ({ const content = remoteContent || localContent - return {content} + return {content} } diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx index ba02bf5116f46..a38bb8efd8483 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/UpgradeWarnings.tsx @@ -46,6 +46,10 @@ const getValidationErrorTitle = (error: ProjectUpgradeEligibilityValidationError return `${error.schema_name}.${error.obj_name}` case 'active_replication_slot': return error.slot_name + case 'project_hibernating': + return 'Project is hibernating' + case 'x86_architecture': + return 'Project is running on x86 architecture' } } @@ -67,6 +71,10 @@ const getValidationErrorDescription = (error: ProjectUpgradeEligibilityValidatio return `Move the ${error.obj_type} to your own schema` case 'active_replication_slot': return 'Drop the active replication slot' + case 'project_hibernating': + return 'The project is currently hibernating and will wake on next supported request' + case 'x86_architecture': + return 'The project is running on x86 architecture and cannot be upgraded' } } diff --git a/apps/studio/components/interfaces/Storage/StorageMenuV2.tsx b/apps/studio/components/interfaces/Storage/StorageMenuV2.tsx index 9ea0cbe55e55d..65d3746b816d3 100644 --- a/apps/studio/components/interfaces/Storage/StorageMenuV2.tsx +++ b/apps/studio/components/interfaces/Storage/StorageMenuV2.tsx @@ -1,4 +1,4 @@ -import { IS_PLATFORM, useParams } from 'common' +import { useParams } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' import { Badge, Menu } from 'ui' @@ -10,6 +10,7 @@ import { useIsAnalyticsBucketsEnabled, useIsVectorBucketsEnabled, } from '@/data/config/project-storage-config-query' +import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { SHORTCUT_IDS, type ShortcutId } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' @@ -25,6 +26,8 @@ export const StorageMenuV2 = () => { const { ref } = useParams() const page = useStorageV2Page() + const { isCli, isPlatform } = useDeploymentMode() + const { storageAnalytics, storageVectors } = useIsFeatureEnabled([ 'storage:analytics', 'storage:vectors', @@ -33,8 +36,8 @@ export const StorageMenuV2 = () => { const isAnalyticsBucketsEnabled = useIsAnalyticsBucketsEnabled({ projectRef: ref }) const isVectorBucketsEnabled = useIsVectorBucketsEnabled({ projectRef: ref }) - const showAnalytics = IS_PLATFORM && storageAnalytics - const showVectors = IS_PLATFORM && storageVectors + const showAnalytics = isPlatform && storageAnalytics + const showVectors = (isPlatform && storageVectors) || isCli useShortcut(SHORTCUT_IDS.NAV_STORAGE_FILES, () => router.push(`/project/${ref}/storage/files`)) useShortcut( @@ -48,7 +51,7 @@ export const StorageMenuV2 = () => { { enabled: showVectors } ) useShortcut(SHORTCUT_IDS.NAV_STORAGE_S3, () => router.push(`/project/${ref}/storage/s3`), { - enabled: IS_PLATFORM, + enabled: isPlatform, }) const bucketTypes = Object.entries(BUCKET_TYPES).filter(([key]) => { @@ -95,7 +98,7 @@ export const StorageMenuV2 = () => { })} - {IS_PLATFORM && ( + {isPlatform && ( <>
diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx index c3099f00e319a..fb0f8c40c4260 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorBucketDialog.tsx @@ -141,15 +141,22 @@ export const CreateVectorBucketDialog = ({ } await createS3VectorsWrapper({ bucketName: values.name }) + toast.success(`Successfully created vector bucket ${values.name}`) } catch (error: any) { - toast.warning( - `Failed to create vector bucket integration: ${error.message}. The bucket will be created but you will need to manually install the integration.` + toast.success( +
+

Successfully created vector bucket {values.name}

+

+ However, bucket integration will need to be manually installed as we ran into an error: +

+

{error.message}

+
, + { duration: 8000 } ) } - setIsLoading(false) + setIsLoading(false) track('storage_bucket_created', { bucketType: 'vector' }) - toast.success(`Successfully created vector bucket ${values.name}`) form.reset() setVisible(false) } diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx index af0a1b2e2063b..a8ff4e49bd92e 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/CreateVectorTableSheet.tsx @@ -34,6 +34,7 @@ import { DocsButton } from '@/components/ui/DocsButton' import { useFDWImportForeignSchemaMutation } from '@/data/fdw/fdw-import-foreign-schema-mutation' import { useVectorBucketIndexCreateMutation } from '@/data/storage/vector-bucket-index-create-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' @@ -104,6 +105,7 @@ interface CreateVectorTableSheetProps { export const CreateVectorTableSheet = ({ bucketName }: CreateVectorTableSheetProps) => { const { data: project } = useSelectedProjectQuery() + const { isCli } = useDeploymentMode() const [visible, setVisible] = useQueryState( 'newTable', @@ -117,7 +119,8 @@ export const CreateVectorTableSheet = ({ bucketName }: CreateVectorTableSheetPro ?.split('supabase_target_schema=')[1] // [Joshen] Can remove this once this restriction is removed - const showIndexCreationNotice = isStagingLocal && !!project && project?.region !== 'us-east-1' + const showIndexCreationNotice = + isStagingLocal && !isCli && !!project && project?.region !== 'us-east-1' const defaultValues = { name: '', diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsErrorState.test.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsErrorState.test.tsx new file mode 100644 index 0000000000000..296c90edbfd1e --- /dev/null +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsErrorState.test.tsx @@ -0,0 +1,52 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { VectorBucketsErrorState } from './VectorBucketsErrorState' +import type { DeploymentMode } from '@/hooks/misc/useDeploymentMode' +import { customRender } from '@/tests/lib/custom-render' +import { ResponseError } from '@/types' + +const { mockUseDeploymentMode } = vi.hoisted(() => ({ + mockUseDeploymentMode: vi.fn<() => DeploymentMode>(), +})) + +vi.mock('@/hooks/misc/useDeploymentMode', () => ({ + useDeploymentMode: mockUseDeploymentMode, +})) + +vi.mock('@/lib/telemetry/track', () => ({ + useTrack: () => vi.fn(), +})) + +const deploymentMode = (overrides: Partial): DeploymentMode => ({ + isPlatform: false, + isCli: false, + isSelfHosted: false, + ...overrides, +}) + +describe('VectorBucketsErrorState', () => { + beforeEach(() => { + mockUseDeploymentMode.mockReset() + }) + + test('CLI: shows the config.toml enable guidance, not the support error', () => { + mockUseDeploymentMode.mockReturnValue(deploymentMode({ isCli: true })) + + customRender() + + expect(screen.getByText('Vector buckets are not enabled')).toBeInTheDocument() + expect(screen.getByText('supabase/config.toml')).toBeInTheDocument() + expect(screen.queryByText('Contact support')).not.toBeInTheDocument() + }) + + test('platform: shows the generic support error', () => { + mockUseDeploymentMode.mockReturnValue(deploymentMode({ isPlatform: true })) + + customRender() + + expect(screen.getByText('Failed to retrieve vector buckets')).toBeInTheDocument() + expect(screen.getByText('Contact support')).toBeInTheDocument() + expect(screen.queryByText('Vector buckets are not enabled')).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsErrorState.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsErrorState.tsx new file mode 100644 index 0000000000000..6bd95e679e30d --- /dev/null +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsErrorState.tsx @@ -0,0 +1,16 @@ +import { VectorBucketsLocalDisabledState } from './VectorBucketsLocalDisabledState' +import AlertError from '@/components/ui/AlertError' +import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode' +import type { ResponseError } from '@/types' + +interface VectorBucketsErrorStateProps { + error: ResponseError | null +} + +export const VectorBucketsErrorState = ({ error }: VectorBucketsErrorStateProps) => { + const { isCli } = useDeploymentMode() + + if (isCli) return + + return +} diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsLocalDisabledState.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsLocalDisabledState.tsx new file mode 100644 index 0000000000000..b58bd8fd8fb14 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/VectorBucketsLocalDisabledState.tsx @@ -0,0 +1,28 @@ +import { Admonition } from 'ui-patterns/admonition' +import { CodeBlock } from 'ui-patterns/CodeBlock' + +const CONFIG_SNIPPET = `[storage.vector] +enabled = true +max_buckets = 10 +max_indexes = 5` + +/** + * Shown on the local CLI when listing vector buckets fails — most commonly + * because `[storage.vector]` is not enabled in `config.toml`. Studio can't read + * `config.toml` directly, so we surface the snippet to enable the feature rather + * than the generic "contact support" error. + */ +export const VectorBucketsLocalDisabledState = () => { + return ( + +

+ To use vector buckets locally, enable them in your{' '} + supabase/config.toml and restart with{' '} + supabase start. +

+ + {CONFIG_SNIPPET} + +
+ ) +} diff --git a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx index 0f628ec1b70e4..4003cc2db9c4d 100644 --- a/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx +++ b/apps/studio/components/interfaces/Storage/VectorBuckets/index.tsx @@ -14,7 +14,7 @@ import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { EmptyBucketState } from '../EmptyBucketState' import { CreateBucketButton } from '../NewBucketButton' import { CreateVectorBucketDialog } from './CreateVectorBucketDialog' -import AlertError from '@/components/ui/AlertError' +import { VectorBucketsErrorState } from './VectorBucketsErrorState' import { AlphaNotice } from '@/components/ui/AlphaNotice' import { useVectorBucketsQuery } from '@/data/storage/vector-buckets-query' import { createNavigationHandler } from '@/lib/navigation' @@ -62,9 +62,7 @@ export const VectorsBuckets = () => { {isLoadingBuckets && } - {isErrorBuckets && ( - - )} + {isErrorBuckets && } {isSuccessBuckets && ( <> diff --git a/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx b/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx deleted file mode 100644 index 3ee91192d7368..0000000000000 --- a/apps/studio/components/layouts/AppLayout/FlyDeprecationBanner.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'common' -import { useRouter } from 'next/router' -import { useEffect, useRef, type ReactNode } from 'react' -import { - Button, - cn, - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogSection, - DialogSectionSeparator, - DialogTitle, - DialogTrigger, -} from 'ui' - -import { HeaderBanner } from '@/components/interfaces/Organization/HeaderBanner' -import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink' -import { - useFlyDeprecationProjects, - type FlyDeprecationProject, -} from '@/hooks/misc/useFlyDeprecationProjects' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' -import { useTrack } from '@/lib/telemetry/track' - -const BANNER_EXPIRES_AT = new Date('2026-06-01T00:00:00Z') -const BACKUP_RESTORE_CLI_URL = - 'https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore' -const DASHBOARD_RESTORE_URL = - 'https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore' -const BRANCHING_DASHBOARD_URL = 'https://supabase.com/docs/guides/deployment/branching/dashboard' -const SUPPORT_EMAIL = 'success@supabase.io' - -export const FlyDeprecationBanner = () => { - const router = useRouter() - - const [acknowledged, setAcknowledged, { isSuccess }] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.FLY_DEPRECATION_2026_05_31, - false - ) - - const isExpired = Date.now() >= BANNER_EXPIRES_AT.getTime() - const onSignIn = router.pathname.startsWith('/sign-in') - - const shouldEvaluate = IS_PLATFORM && !isExpired && !onSignIn && isSuccess && !acknowledged - - const { isReady, primaries, branches } = useFlyDeprecationProjects({ enabled: shouldEvaluate }) - - const hasFlyResources = primaries.length > 0 || branches.length > 0 - - const track = useTrack() - - const exposedRef = useRef(false) - useEffect(() => { - if (!shouldEvaluate || !isReady || !hasFlyResources || exposedRef.current) return - exposedRef.current = true - track('fly_deprecation_banner_exposed', { - primaryCount: primaries.length, - branchCount: branches.length, - }) - }, [shouldEvaluate, isReady, hasFlyResources, primaries.length, branches.length, track]) - - if (!shouldEvaluate || !isReady || !hasFlyResources) return null - - const onDismiss = () => { - track('fly_deprecation_banner_dismissed', { - primaryCount: primaries.length, - branchCount: branches.length, - }) - setAcknowledged(true) - } - - const title = - primaries.length > 0 && branches.length > 0 - ? 'Action required: Fly.io project and branch suspensions begin May 31' - : primaries.length > 0 - ? 'Action required: Fly.io project suspensions begin May 31' - : 'Action required: Fly.io branch suspensions begin May 31' - - return ( - } - onDismiss={onDismiss} - /> - ) -} - -const FlyDeprecationDialog = ({ - primaries, - branches, -}: { - primaries: FlyDeprecationProject[] - branches: FlyDeprecationProject[] -}) => { - return ( - - - View affected projects - - - - Fly.io deprecation: suspensions begin May 31, 2026 - - Supabase will begin suspending projects and branches still running on Fly.io - infrastructure on May 31, 2026. - - - - - - -

- To preserve your data, migrate each project to Supabase's general infrastructure: -

-
    -
  1. - Back up your database using the{' '} - Supabase CLI (or take a{' '} - Dashboard backup). -
  2. -
  3. Create a new project on Supabase's general infrastructure.
  4. -
  5. Restore the backup into the new project.
  6. -
- - } - /> - - -

Merge preview branches before May 31. For persistent branches:

-
    -
  1. - Take a{' '} - - snapshot of the branch database - {' '} - before shutting it down. -
  2. -
  3. - - Deploy a new persistent branch - {' '} - on Supabase's general infrastructure. -
  4. -
  5. Restore your data manually from the snapshot.
  6. -
- - } - /> - - -

- Questions or need an extension? Email{' '} - - {SUPPORT_EMAIL} - - . -

-
- - - - - - -
-
- ) -} - -const MAX_LISTED = 5 - -const ProjectList = ({ - label, - items, - instructions, -}: { - label: string - items: FlyDeprecationProject[] - instructions: ReactNode -}) => { - if (items.length === 0) return null - const visible = items.slice(0, MAX_LISTED) - const remaining = items.length - visible.length - return ( - <> - -

- {label} ({items.length}) -

- {items.length === 1 ? ( -

- {items[0].name} ({items[0].orgName}) -

- ) : ( -
    - {visible.map((p) => ( -
  • - {p.name} ({p.orgName}) -
  • - ))} - {remaining > 0 && ( -
  • …and {remaining} more.
  • - )} -
- )} -
- {instructions} - - ) -} diff --git a/apps/studio/data/config/project-storage-config-query.test.tsx b/apps/studio/data/config/project-storage-config-query.test.tsx new file mode 100644 index 0000000000000..b0bdc6469c083 --- /dev/null +++ b/apps/studio/data/config/project-storage-config-query.test.tsx @@ -0,0 +1,113 @@ +import { waitFor } from '@testing-library/react' +import { HttpResponse } from 'msw' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { + useIsVectorBucketsEnabled, + type ProjectStorageConfigData, +} from './project-storage-config-query' +import type { DeploymentMode } from '@/hooks/misc/useDeploymentMode' +import { customRenderHook } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +const { mockIsPlatform, mockUseDeploymentMode } = vi.hoisted(() => ({ + mockIsPlatform: { value: false }, + mockUseDeploymentMode: vi.fn<() => DeploymentMode>(), +})) + +// `useProjectStorageConfigQuery` (same module as the hook under test) gates its +// fetch on the build-time `IS_PLATFORM` constant — mock it so the query fires in +// the platform cases. +vi.mock('@/lib/constants', async () => { + const actual = await vi.importActual>('@/lib/constants') + return { + ...actual, + get IS_PLATFORM() { + return mockIsPlatform.value + }, + } +}) + +vi.mock('@/hooks/misc/useDeploymentMode', () => ({ + useDeploymentMode: mockUseDeploymentMode, +})) + +const createStorageConfig = (vectorBucketsEnabled: boolean): ProjectStorageConfigData => + ({ + capabilities: { iceberg_catalog: false, list_v2: false }, + databasePoolMode: 'transaction', + external: { upstreamTarget: 'main' }, + features: { + icebergCatalog: { enabled: false, maxCatalogs: 0, maxNamespaces: 0, maxTables: 0 }, + imageTransformation: { enabled: false }, + s3Protocol: { enabled: false }, + vectorBuckets: { enabled: vectorBucketsEnabled, maxBuckets: 0, maxIndexes: 0 }, + }, + fileSizeLimit: 0, + migrationVersion: 'v1', + }) as ProjectStorageConfigData + +const deploymentMode = (overrides: Partial): DeploymentMode => ({ + isPlatform: false, + isCli: false, + isSelfHosted: false, + ...overrides, +}) + +describe('useIsVectorBucketsEnabled', () => { + beforeEach(() => { + mockIsPlatform.value = false + mockUseDeploymentMode.mockReset() + }) + + test('platform + storage config flag enabled: true', async () => { + mockIsPlatform.value = true + mockUseDeploymentMode.mockReturnValue(deploymentMode({ isPlatform: true })) + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/config/storage', + response: () => HttpResponse.json(createStorageConfig(true)), + }) + + const { result } = customRenderHook(() => useIsVectorBucketsEnabled({ projectRef: 'default' })) + + await waitFor(() => expect(result.current).toBe(true)) + }) + + test('platform + storage config flag disabled: false', async () => { + mockIsPlatform.value = true + mockUseDeploymentMode.mockReturnValue(deploymentMode({ isPlatform: true })) + + let configRequested = false + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/config/storage', + response: () => { + configRequested = true + return HttpResponse.json(createStorageConfig(false)) + }, + }) + + const { result } = customRenderHook(() => useIsVectorBucketsEnabled({ projectRef: 'default' })) + + await waitFor(() => expect(configRequested).toBe(true)) + expect(result.current).toBe(false) + }) + + test('CLI: true regardless of the storage config flag', () => { + // Query is disabled off-platform, so no storage config endpoint is needed. + mockUseDeploymentMode.mockReturnValue(deploymentMode({ isCli: true })) + + const { result } = customRenderHook(() => useIsVectorBucketsEnabled({ projectRef: 'default' })) + + expect(result.current).toBe(true) + }) + + test('self-hosted: false', () => { + mockUseDeploymentMode.mockReturnValue(deploymentMode({ isSelfHosted: true })) + + const { result } = customRenderHook(() => useIsVectorBucketsEnabled({ projectRef: 'default' })) + + expect(result.current).toBe(false) + }) +}) diff --git a/apps/studio/data/config/project-storage-config-query.ts b/apps/studio/data/config/project-storage-config-query.ts index 37e21ec49e59e..987a0bbe36491 100644 --- a/apps/studio/data/config/project-storage-config-query.ts +++ b/apps/studio/data/config/project-storage-config-query.ts @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { configKeys } from './keys' import { components } from '@/data/api' import { get, handleError } from '@/data/fetchers' +import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode' import { IS_PLATFORM } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' @@ -60,6 +61,8 @@ export const useIsAnalyticsBucketsEnabled = ({ projectRef }: { projectRef?: stri export const useIsVectorBucketsEnabled = ({ projectRef }: { projectRef?: string }) => { const { data } = useProjectStorageConfigQuery({ projectRef }) - const isVectorBucketsEnabled = !!data?.features.vectorBuckets?.enabled + const { isCli, isPlatform } = useDeploymentMode() + + const isVectorBucketsEnabled = isCli || (isPlatform && !!data?.features.vectorBuckets?.enabled) return isVectorBucketsEnabled } diff --git a/apps/studio/data/misc/keys.ts b/apps/studio/data/misc/keys.ts index 02ec349d343ba..36d2eed504ef6 100644 --- a/apps/studio/data/misc/keys.ts +++ b/apps/studio/data/misc/keys.ts @@ -5,4 +5,5 @@ export const miscKeys = { ipAddress: () => ['ip-address'] as const, clockSkew: () => ['clock-skew'] as const, enabledFeaturesOverride: () => ['enabled-features-override'] as const, + localS3Keys: () => ['local-s3-keys'] as const, } diff --git a/apps/studio/data/misc/local-s3-keys-query.ts b/apps/studio/data/misc/local-s3-keys-query.ts new file mode 100644 index 0000000000000..1ca08be065d22 --- /dev/null +++ b/apps/studio/data/misc/local-s3-keys-query.ts @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query' + +import { miscKeys } from './keys' +import { fetchHandler } from '@/data/fetchers' +import { BASE_PATH, IS_PLATFORM } from '@/lib/constants' +import type { ResponseError, UseCustomQueryOptions } from '@/types' + +export async function getLocalS3Keys() { + try { + const data = await fetchHandler(`${BASE_PATH}/api/get-s3-keys`).then((res) => res.json()) + return data as { accessKey?: string; secretKey?: string } + } catch (error) { + throw error + } +} + +export type LocalS3KeysData = Awaited> +export type LocalS3KeysError = ResponseError + +/** + * Specifically only for local CLI - to use the S3 keys as defined in the env file + */ +export const useLocalS3KeysQuery = ({ + enabled = true, + ...options +}: UseCustomQueryOptions = {}) => + useQuery({ + queryKey: miscKeys.localS3Keys(), + queryFn: () => getLocalS3Keys(), + enabled: enabled && !IS_PLATFORM, + ...options, + }) diff --git a/apps/studio/data/storage/s3-vectors-wrapper-create-mutation.ts b/apps/studio/data/storage/s3-vectors-wrapper-create-mutation.ts index da9f22f595b4a..c77b8c329a6e0 100644 --- a/apps/studio/data/storage/s3-vectors-wrapper-create-mutation.ts +++ b/apps/studio/data/storage/s3-vectors-wrapper-create-mutation.ts @@ -1,5 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { IS_PLATFORM } from 'common' +import { useLocalS3KeysQuery } from '../misc/local-s3-keys-query' import { useS3AccessKeyCreateMutation } from './s3-access-key-create-mutation' import { WRAPPERS } from '@/components/interfaces/Integrations/Wrappers/Wrappers.constants' import { getVectorURI } from '@/components/interfaces/Storage/StorageSettings/StorageSettings.utils' @@ -11,14 +13,29 @@ import { import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' import { FDWCreateVariables, useFDWCreateMutation } from '@/data/fdw/fdw-create-mutation' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export const useS3VectorsWrapperCreateMutation = () => { const { data: project } = useSelectedProjectQuery() + const { data: localKeys } = useLocalS3KeysQuery() + const { isPlatform } = useDeploymentMode() const { data: settings } = useProjectSettingsV2Query({ projectRef: project?.ref }) const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.storage_endpoint || settings?.app_config?.endpoint + + /** + * [Joshen] Endpoint for vectors FDW needs to use docker domain + * Not sure if this affects other areas of the dashboard hence why conditionally rendering here + * instead of updating `lib/api/self-hosted/settings -> api_config` + */ + const port = + !isPlatform && !!settings + ? new URL(`${protocol}://${settings.app_config?.endpoint}`).port + : null + const endpoint = !isPlatform + ? `host.docker.internal:${port}` + : settings?.app_config?.storage_endpoint || settings?.app_config?.endpoint const wrapperMeta = WRAPPERS.find((wrapper) => wrapper.name === 's3_vectors_wrapper') @@ -30,13 +47,32 @@ export const useS3VectorsWrapperCreateMutation = () => { const { mutateAsync: createS3AccessKey, isPending: isCreatingS3AccessKey } = useS3AccessKeyCreateMutation() - const { mutateAsync: createFDW, isPending: isCreatingFDW } = useFDWCreateMutation() + // [Joshen] Silence the error, handled upstream + const { mutateAsync: createFDW, isPending: isCreatingFDW } = useFDWCreateMutation({ + onError: () => {}, + }) const mutateAsync = async ({ bucketName }: { bucketName: string }) => { - const createS3KeyData = await createS3AccessKey({ - projectRef: project?.ref, - description: getVectorBucketS3KeyName(bucketName), - }) + let accessKey: string | undefined + let secretKey: string | undefined + + if (IS_PLATFORM) { + const createS3KeyData = await createS3AccessKey({ + projectRef: project?.ref, + description: getVectorBucketS3KeyName(bucketName), + }) + accessKey = createS3KeyData.access_key + secretKey = createS3KeyData.secret_key + } else { + accessKey = localKeys?.accessKey + secretKey = localKeys?.secretKey + } + + if (!accessKey || !secretKey) { + throw new Error( + IS_PLATFORM ? 'Failed to obtain S3 keys from the API' : 'Local S3 keys are not configured' + ) + } const wrapperName = getVectorBucketFDWName(bucketName) const serverName = getVectorBucketFDWServerName(bucketName) @@ -48,8 +84,8 @@ export const useS3VectorsWrapperCreateMutation = () => { formState: { wrapper_name: wrapperName, server_name: serverName, - vault_access_key_id: createS3KeyData?.access_key, - vault_secret_access_key: createS3KeyData?.secret_key, + vault_access_key_id: accessKey, + vault_secret_access_key: secretKey, aws_region: settings!.region, endpoint_url: getVectorURI(project?.ref ?? '', protocol, endpoint), }, diff --git a/apps/studio/hooks/misc/useFlyDeprecationProjects.ts b/apps/studio/hooks/misc/useFlyDeprecationProjects.ts deleted file mode 100644 index c2fc392c87dff..0000000000000 --- a/apps/studio/hooks/misc/useFlyDeprecationProjects.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query' -import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { PROVIDERS } from '@/lib/constants/infrastructure' - -export interface FlyDeprecationProject { - ref: string - name: string - orgSlug: string - orgName: string -} - -export function useFlyDeprecationProjects({ enabled }: { enabled: boolean }) { - const { data: selectedProject } = useSelectedProjectQuery() - const { data: selectedOrg } = useSelectedOrganizationQuery() - - const orgSlug = selectedOrg?.slug - const orgName = selectedOrg?.name ?? '' - - const { data: orgProjectsData, isFetched } = useOrgProjectsInfiniteQuery( - { slug: orgSlug }, - { enabled: enabled && Boolean(orgSlug), staleTime: 30 * 60 * 1000 } - ) - - if (!enabled || !orgSlug) { - return { isReady: false, primaries: [], branches: [] } - } - - const byRef = new Map() - - if (selectedProject?.cloud_provider === PROVIDERS.FLY.id) { - byRef.set(selectedProject.ref, { - ref: selectedProject.ref, - name: selectedProject.name, - orgSlug, - orgName, - isBranch: Boolean(selectedProject.parent_project_ref), - }) - } - - const orgProjects = orgProjectsData?.pages.flatMap((page) => page.projects) ?? [] - for (const p of orgProjects) { - if (p.cloud_provider !== PROVIDERS.FLY.id) continue - if (byRef.has(p.ref)) continue - byRef.set(p.ref, { - ref: p.ref, - name: p.name, - orgSlug, - orgName, - isBranch: Boolean(p.is_branch), - }) - } - - const all = Array.from(byRef.values()) - const primaries: FlyDeprecationProject[] = [] - const branches: FlyDeprecationProject[] = [] - for (const { isBranch, ...rest } of all) { - ;(isBranch ? branches : primaries).push(rest) - } - - return { isReady: isFetched, primaries, branches } -} diff --git a/apps/studio/lib/api/self-hosted/settings.test.ts b/apps/studio/lib/api/self-hosted/settings.test.ts index d4458dc34c869..4903332909cd9 100644 --- a/apps/studio/lib/api/self-hosted/settings.test.ts +++ b/apps/studio/lib/api/self-hosted/settings.test.ts @@ -51,7 +51,7 @@ describe('api/self-hosted/settings', () => { expect(settings.db_port).toBe(5432) expect(settings.db_user).toBe('postgres') expect(settings.ref).toBe('default') - expect(settings.region).toBe('ap-southeast-1') + expect(settings.region).toBe('local') expect(settings.status).toBe('ACTIVE_HEALTHY') expect(settings.ssl_enforced).toBe(false) }) diff --git a/apps/studio/lib/api/self-hosted/settings.ts b/apps/studio/lib/api/self-hosted/settings.ts index f7b8a40ae06d7..4a35bb2fbb8e0 100644 --- a/apps/studio/lib/api/self-hosted/settings.ts +++ b/apps/studio/lib/api/self-hosted/settings.ts @@ -40,7 +40,7 @@ export function getProjectSettings() { process.env.AUTH_JWT_SECRET ?? 'super-secret-jwt-token-with-at-least-32-characters-long', name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', ref: 'default', - region: 'ap-southeast-1', + region: 'local', service_api_keys: [ { api_key: process.env.SUPABASE_SERVICE_KEY ?? '', diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 216e5a03f32be..33d4d6b42fe34 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -61,6 +61,7 @@ import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from '@/lib/const import { TimezoneProvider, useTimezone } from '@/lib/datetime' import { ProfileProvider } from '@/lib/profile' import { Telemetry } from '@/lib/telemetry' +import { ToastErrorTracker } from '@/lib/toast-errors' import { Toaster } from '@/lib/toaster' import { AiAssistantStateContextProvider } from '@/state/ai-assistant-state' import type { AppPropsWithLayout } from '@/types' @@ -234,6 +235,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { + {!isTestEnv && ( )} diff --git a/apps/studio/pages/api/get-s3-keys.ts b/apps/studio/pages/api/get-s3-keys.ts new file mode 100644 index 0000000000000..f6331fbf0bf6d --- /dev/null +++ b/apps/studio/pages/api/get-s3-keys.ts @@ -0,0 +1,10 @@ +import { NextApiRequest, NextApiResponse } from 'next' + +const accessKey = process.env.S3_PROTOCOL_ACCESS_KEY_ID +const secretKey = process.env.S3_PROTOCOL_ACCESS_KEY_SECRET + +const handler = async (_req: NextApiRequest, res: NextApiResponse) => { + return res.status(200).json({ accessKey: accessKey, secretKey }) +} + +export default handler diff --git a/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/index.ts b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/index.ts new file mode 100644 index 0000000000000..da26596bbe3a3 --- /dev/null +++ b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/index.ts @@ -0,0 +1,37 @@ +import { createClient } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' + +import apiWrapper from '@/lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +// eslint-disable-next-line import/no-anonymous-default-export +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + case 'DELETE': + return handleDelete(req, res) + default: + res.setHeader('Allow', ['GET', 'PATCH', 'DELETE']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { data, error } = await supabase.storage.vectors.getBucket(id as string) + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data.vectorBucket) +} + +const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { data, error } = await supabase.storage.vectors.deleteBucket(id as string) + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data) +} diff --git a/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/indexes/[indexName].ts b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/indexes/[indexName].ts new file mode 100644 index 0000000000000..671361ae94fae --- /dev/null +++ b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/indexes/[indexName].ts @@ -0,0 +1,32 @@ +import { createClient } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' + +import apiWrapper from '@/lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +// eslint-disable-next-line import/no-anonymous-default-export +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'DELETE': + return handleDelete(req, res) + + default: + res.setHeader('Allow', ['GET', 'POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} +const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { + const { id, indexName } = req.query + + const { data, error } = await supabase.storage.vectors + .from(id as string) + .deleteIndex(indexName as string) + + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data) +} diff --git a/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/indexes/index.ts b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/indexes/index.ts new file mode 100644 index 0000000000000..272fdc74a9df4 --- /dev/null +++ b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/[id]/indexes/index.ts @@ -0,0 +1,58 @@ +import { createClient } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' + +import apiWrapper from '@/lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +// eslint-disable-next-line import/no-anonymous-default-export +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + case 'POST': + return handlePost(req, res) + + default: + res.setHeader('Allow', ['GET', 'POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + + const { data, error } = await supabase.storage.vectors + .from(id as string) + .listIndexes({ maxResults: 100 }) + + if (error) return res.status(500).json({ error: { message: error.message } }) + + const indexes = await Promise.all( + data.indexes.map(async ({ indexName }) => { + return (await supabase.storage.vectors.from(id as string).getIndex(indexName)).data?.index + }) + ) + + return res.status(200).json({ indexes, nextToken: data.nextToken }) +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { indexName, dataType, dimension, distanceMetric, metadataKeys } = req.body + const payload = { + indexName, + dataType, + dimension, + distanceMetric, + metadataConfiguration: { nonFilterableMetadataKeys: metadataKeys }, + } + + const { data, error } = await supabase.storage.vectors.from(id as string).createIndex(payload) + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data) +} diff --git a/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/index.ts b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/index.ts new file mode 100644 index 0000000000000..2d1b91ee0535e --- /dev/null +++ b/apps/studio/pages/api/platform/storage/[ref]/vector-buckets/index.ts @@ -0,0 +1,38 @@ +import { createClient } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' + +import apiWrapper from '@/lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +// eslint-disable-next-line import/no-anonymous-default-export +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + case 'POST': + return handlePost(req, res) + + default: + res.setHeader('Allow', ['GET', 'POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleGet = async (_req: NextApiRequest, res: NextApiResponse) => { + const { data, error } = await supabase.storage.vectors.listBuckets() + if (error) return res.status(500).json({ error: { message: error.message } }) + + return res.status(200).json(data) +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { bucketName } = req.body + const { data, error } = await supabase.storage.vectors.createBucket(bucketName) + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data) +} diff --git a/apps/studio/pages/project/[ref]/storage/vectors/index.tsx b/apps/studio/pages/project/[ref]/storage/vectors/index.tsx index edc2cd6a79e7a..8a277b627ae76 100644 --- a/apps/studio/pages/project/[ref]/storage/vectors/index.tsx +++ b/apps/studio/pages/project/[ref]/storage/vectors/index.tsx @@ -1,4 +1,4 @@ -import { useParams } from 'common' +import { IS_PLATFORM, useParams } from 'common' import { BucketsUpgradePlan } from '@/components/interfaces/Storage/BucketsUpgradePlan' import { VectorsBuckets } from '@/components/interfaces/Storage/VectorBuckets' @@ -23,10 +23,12 @@ const StorageVectorsPage: NextPageWithLayout = () => { project?.region ?? '' ) - if (!isAvailableInProjectRegion) { + if (IS_PLATFORM && !isAvailableInProjectRegion) { return - } else if (!isVectorBucketsEnabled) { + } else if (IS_PLATFORM && !isVectorBucketsEnabled) { return + } else if (!isVectorBucketsEnabled) { + return null } else { return } diff --git a/apps/studio/tests/pages/project/[ref]/storage/vectors/index.test.tsx b/apps/studio/tests/pages/project/[ref]/storage/vectors/index.test.tsx new file mode 100644 index 0000000000000..f5c0cd53e3e0f --- /dev/null +++ b/apps/studio/tests/pages/project/[ref]/storage/vectors/index.test.tsx @@ -0,0 +1,155 @@ +import { screen, waitFor } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import type { ProjectStorageConfigData } from '@/data/config/project-storage-config-query' +import type { ProjectDetail } from '@/data/projects/project-detail-query' +import { API_URL } from '@/lib/constants' +import StorageVectorsPage from '@/pages/project/[ref]/storage/vectors' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock, mswServer } from '@/tests/lib/msw' + +const { mockIsPlatform } = vi.hoisted(() => ({ + mockIsPlatform: { value: true }, +})) + +// `IS_PLATFORM` is a build-time constant, so it can't be driven over the +// network — mock it in both modules that read it (the page imports from +// `common`; the data hooks read from `@/lib/constants`). Everything else the +// page branches on comes from real queries via MSW. +vi.mock('common', async (importOriginal) => { + const actual = await importOriginal>() + return { + ...actual, + useParams: () => ({ ref: 'default' }), + get IS_PLATFORM() { + return mockIsPlatform.value + }, + } +}) + +vi.mock('@/lib/constants', async (importOriginal) => { + const actual = await importOriginal>() + return { + ...actual, + get IS_PLATFORM() { + return mockIsPlatform.value + }, + } +}) + +// Stub the heavy leaf children — each fires its own queries and renders a large +// tree. We only assert which branch the page picks. Keep the real +// `VECTOR_BUCKETS_AVAILABLE_REGIONS` so the region gate runs for real. +vi.mock( + '@/components/interfaces/Storage/VectorBuckets/RegionLimitation', + async (importOriginal) => ({ + ...(await importOriginal>()), + RegionLimitation: () =>
region-limitation
, + }) +) + +vi.mock('@/components/interfaces/Storage/VectorBuckets', () => ({ + VectorsBuckets: () =>
vectors-buckets
, +})) + +vi.mock('@/components/interfaces/Storage/BucketsUpgradePlan', () => ({ + BucketsUpgradePlan: ({ type }: { type: string }) =>
buckets-upgrade-plan-{type}
, +})) + +const AVAILABLE_REGION = 'us-east-1' +const UNAVAILABLE_REGION = 'ap-south-1' + +const mockProject = (region: string) => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref', + // The page only reads `region` off the project + response: () => HttpResponse.json({ region } as unknown as ProjectDetail), + }) +} + +const mockStorageConfig = (vectorBucketsEnabled: boolean) => { + addAPIMock({ + method: 'get', + path: '/platform/projects/:ref/config/storage', + response: () => + HttpResponse.json({ + capabilities: { iceberg_catalog: false, list_v2: false }, + databasePoolMode: 'transaction', + external: { upstreamTarget: 'main' }, + features: { + icebergCatalog: { enabled: false, maxCatalogs: 0, maxNamespaces: 0, maxTables: 0 }, + imageTransformation: { enabled: false }, + s3Protocol: { enabled: false }, + vectorBuckets: { enabled: vectorBucketsEnabled, maxBuckets: 0, maxIndexes: 0 }, + }, + fileSizeLimit: 0, + migrationVersion: 'v1', + } as ProjectStorageConfigData), + }) +} + +const mockDeploymentMode = (isCli: boolean) => { + mswServer.use( + http.get(`${API_URL}/platform/deployment-mode`, () => HttpResponse.json({ is_cli_mode: isCli })) + ) +} + +describe('StorageVectorsPage', () => { + beforeEach(() => { + mockIsPlatform.value = true + mockProject(AVAILABLE_REGION) + }) + + test('platform + region not supported: shows region limitation', async () => { + mockProject(UNAVAILABLE_REGION) + mockStorageConfig(true) + + customRender() + + expect(await screen.findByText('region-limitation')).toBeInTheDocument() + expect(screen.queryByText('vectors-buckets')).not.toBeInTheDocument() + }) + + test('platform + supported region + not enabled: shows upgrade plan', async () => { + mockStorageConfig(false) + + customRender() + + expect(await screen.findByText('buckets-upgrade-plan-vector')).toBeInTheDocument() + expect(screen.queryByText('vectors-buckets')).not.toBeInTheDocument() + }) + + test('platform + supported region + enabled: shows vector buckets', async () => { + mockStorageConfig(true) + + customRender() + + expect(await screen.findByText('vectors-buckets')).toBeInTheDocument() + }) + + test('CLI (non-platform, enabled): shows vector buckets, skips region/upgrade gates', async () => { + mockIsPlatform.value = false + // Even an unsupported region shouldn't gate off-platform + mockProject(UNAVAILABLE_REGION) + mockDeploymentMode(true) + + customRender() + + expect(await screen.findByText('vectors-buckets')).toBeInTheDocument() + expect(screen.queryByText('region-limitation')).not.toBeInTheDocument() + }) + + test('self-hosted (non-platform): renders nothing', async () => { + mockIsPlatform.value = false + mockDeploymentMode(false) + + const { container } = customRender() + + // `useDeploymentMode` defaults to CLI during its loading window, so the page + // briefly renders before resolving to self-hosted — wait for it to settle. + await waitFor(() => expect(container).toBeEmptyDOMElement()) + expect(screen.queryByText('vectors-buckets')).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/turbo.jsonc b/apps/studio/turbo.jsonc index 483593c584373..53b62b1c357fe 100644 --- a/apps/studio/turbo.jsonc +++ b/apps/studio/turbo.jsonc @@ -107,6 +107,8 @@ "VERCEL_GIT_COMMIT_SHA", "SNIPPETS_MANAGEMENT_FOLDER", "EDGE_FUNCTIONS_MANAGEMENT_FOLDER", + "S3_PROTOCOL_ACCESS_KEY_ID", + "S3_PROTOCOL_ACCESS_KEY_SECRET", ], "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*"], }, diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index 934a83da7f214..551a7613f9a46 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -3203,20 +3203,6 @@ module.exports = [ destination: '/dashboard/redeem?code=:code', permanent: false, }, - // Reference pages use hash anchors for sections; redirect the legacy - // path-style /docs/reference//introduction (and versioned variants) - // back to the base reference URL. Order matters: introduction first so it - // strips to a bare URL, then the section rules add a hash anchor. - { - permanent: true, - source: '/docs/reference/:lib/:version(v\\d+)/:section', - destination: '/docs/reference/:lib/:version#:section', - }, - { - permanent: true, - source: '/docs/reference/:lib/:section((?!v\\d+$)[^/]+)', - destination: '/docs/reference/:lib#:section', - }, // Legacy product .txt URLs → new .md routes { permanent: true, source: '/llms/homepage.txt', destination: '/homepage.md' }, { permanent: true, source: '/llms/auth.txt', destination: '/auth.md' }, diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 07d186d944d1e..362b857dd5baf 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -1205,6 +1205,27 @@ export interface paths { patch?: never trace?: never } + '/v1/projects/{ref}/database/backups/schedule': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Gets the backup schedule for a project */ + get: operations['v1-get-backup-schedule'] + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * Updates the backup schedule time for a project + * @description Sets the time at which the daily backup runs. The change takes effect on the next backup window that includes the new time. If the new time has already passed for today, the first backup at the new time will occur the following day. It can only be updated 3 times per 24 hours. + */ + patch: operations['v1-update-backup-schedule'] + trace?: never + } '/v1/projects/{ref}/database/backups/undo': { parameters: { query?: never @@ -2503,7 +2524,11 @@ export interface components { project_ref: string /** Format: date-time */ review_requested_at?: string - /** @enum {string} */ + /** + * @deprecated + * @description This field is deprecated. List action runs to get branch status instead. + * @enum {string} + */ status: | 'CREATING_PROJECT' | 'RUNNING_MIGRATIONS' @@ -3748,6 +3773,14 @@ export interface components { /** @enum {string} */ type: 'active_replication_slot' } + | { + /** @enum {string} */ + type: 'x86_architecture' + } + | { + /** @enum {string} */ + type: 'project_hibernating' + } )[] warnings: { /** @enum {string} */ @@ -4690,6 +4723,19 @@ export interface components { release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview' target_version: string } + V1BackupScheduleResponse: { + /** + * @description Time of day to schedule daily backups, in UTC. Format: HH:MM:SS. + * @example 04:00:00 + */ + schedule_for: string + /** + * Format: date-time + * @description Timestamp of when the backup schedule was last updated. + * @example 2026-05-04T14:40:44+00:00 + */ + updated_at: string + } V1BackupsResponse: { backups: { id: number @@ -4945,6 +4991,7 @@ export interface components { | 'ipv4' | 'pitr.available_variants' | 'log_drains' + | 'audit_log_drains' | 'branching_limit' | 'branching_persistent' | 'auth.mfa_phone' @@ -4962,6 +5009,7 @@ export interface components { | 'auth.custom_oauth.max_providers' | 'backup.retention_days' | 'backup.restore_to_new_project' + | 'backup.schedule' | 'function.max_count' | 'function.size_limit_mb' | 'realtime.max_concurrent_users' @@ -5298,6 +5346,16 @@ export interface components { V1UndoBody: { name: string } + /** @example { + * "schedule_for": "04:00:00" + * } */ + V1UpdateBackupScheduleBody: { + /** + * @description Time of day to schedule daily backups, in UTC. Format: HH:MM:SS. + * @example 04:00:00 + */ + schedule_for: string + } /** @example { * "name": "Hello World", * "body": "Deno.serve(() => new Response('Hello again!'))", @@ -9827,6 +9885,145 @@ export interface operations { } } } + 'v1-get-backup-schedule': { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['V1BackupScheduleResponse'] + } + } + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Feature requires a higher plan */ + 402: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Forbidden action */ + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Project or backup schedule not found */ + 404: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Failed to retrieve backup schedule */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + 'v1-update-backup-schedule': { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['V1UpdateBackupScheduleBody'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['V1BackupScheduleResponse'] + } + } + /** @description Invalid schedule_for format */ + 400: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Feature requires a higher plan */ + 402: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Forbidden action */ + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Project or backup schedule not found */ + 404: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Failed to update backup schedule */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } 'v1-undo': { parameters: { query?: never diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index fb6cc904bf121..5deaa36a0963e 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -69,7 +69,6 @@ export const LOCAL_STORAGE_KEYS = { EXPAND_NAVIGATION_PANEL: 'supabase-expand-navigation-panel', GITHUB_AUTHORIZATION_STATE: 'supabase-github-authorization-state', // Notice banner keys - FLY_DEPRECATION_2026_05_31: 'fly-deprecation-2026-05-31-dismissed', API_KEYS_FEEDBACK_DISMISSED: (ref: string) => `supabase-api-keys-feedback-dismissed-${ref}`, TERMS_OF_SERVICE_UPDATE: 'terms-of-service-update-2026-06-06', SUPAVISOR_MAINTENANCE: (ref: string) => `supavisor-maintenance-2026-05-21-${ref}`, diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index ce29ea272f537..4899cfce013af 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2450,7 +2450,6 @@ export interface LogDrainSaveButtonClickedEvent { | 'clickhouse' | 'webhook' | 'datadog' - | 'elastic' | 'loki' | 'sentry' | 's3' @@ -2481,7 +2480,6 @@ export interface LogDrainRemovedEvent { | 'clickhouse' | 'webhook' | 'datadog' - | 'elastic' | 'loki' | 'sentry' | 's3' @@ -2919,41 +2917,11 @@ export interface ComputeBadgeUpgradeClickedEvent { properties: { computeSize: string planId: string - upgradeType: 'pro_upgrade' | 'free_micro_upgrade' | 'compute_upgrade' + upgradeType: 'free_micro_upgrade' | 'compute_upgrade' } groups: TelemetryGroups } -/** - * Fly.io deprecation banner rendered for a user with at least one Fly.io project or branch. - * - * @group Events - * @source studio - */ -export interface FlyDeprecationBannerExposedEvent { - action: 'fly_deprecation_banner_exposed' - groups: TelemetryGroups - properties: { - primaryCount: number - branchCount: number - } -} - -/** - * User dismissed the Fly.io deprecation banner. - * - * @group Events - * @source studio - */ -export interface FlyDeprecationBannerDismissedEvent { - action: 'fly_deprecation_banner_dismissed' - groups: TelemetryGroups - properties: { - primaryCount: number - branchCount: number - } -} - /** * User dismissed the free Micro upgrade banner. * @@ -3461,8 +3429,6 @@ export type TelemetryEvent = | OrgMenuBackClickedEvent | OrgMenuItemClickedEvent | ComputeBadgeUpgradeClickedEvent - | FlyDeprecationBannerExposedEvent - | FlyDeprecationBannerDismissedEvent | FreeMicroUpgradeBannerDismissedEvent | FreeMicroUpgradeBannerCtaClickedEvent | AccessTokenCreatedEvent diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index aab030e4956fa..579d8852c890b 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -175,6 +175,7 @@ allow_list = [ "Authy", "B-tree", "Better Stack", + "Berri", "Basejump", "BigQuery", "Bitbucket", @@ -187,6 +188,7 @@ allow_list = [ "CAPTCHA", "Cartes Bancaires", "[Cc]ertbot", + "chatbot", "ChatGPT", "Citus", "ClickHouse", @@ -228,6 +230,7 @@ allow_list = [ "FastMCP", "Fiberplane", "Figma", + "Firecrawl", "Firestore", "Fivetran", "Floyd-Warshall", @@ -290,6 +293,7 @@ allow_list = [ "Mailpit", "Mailtrap", "Mansueli", + "Markprompt", "Metabase", "[Mm]in[Ii][Oo]", "[Mm]itigations", @@ -498,6 +502,7 @@ allow_list = [ "vCPUs", "vecs", "vs", + "[Ww]alkthrough", "watchOS", "WebCrypto", "[Xx]min",