Merge branch 'master' into dnywh/prototype/warehouse

# Conflicts:
#	apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx
#	apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.utils.test.ts
#	apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.utils.ts
#	apps/studio/components/interfaces/Database/Publications/PublicationsTableItem.tsx
This commit is contained in:
Danny White committed 2026-07-09 13:38:26 -04:00
commit 60a0ddffc9
374 files changed
+8222 -2152

No files matched your search

+3 -1
View File
@@ -8,6 +8,8 @@
/apps/studio/ @supabase/Dashboard
/apps/docs/ @supabase/docs
/apps/www/ @supabase/marketing
/apps/www/public/images/blog @supabase/marketing
/apps/www/lib/redirects.js
@@ -19,4 +21,4 @@
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
/apps/studio/pages/new/index.tsx @supabase/security
/packages/shared-data/compute-disk-limits.ts @supabase/infra @supabase/platform
/packages/shared-data/compute-disk-limits.ts @supabase/platform
+36 -2
View File
@@ -15,8 +15,42 @@ permissions:
contents: read
jobs:
# Cheap, always-on gate: verifies the eval harness can reach the MCP server
# (its one real tool, search_docs). Runs on every push/PR — no OpenAI, no full
# eval suite — so a broken MCP connection is caught early with an actionable
# message. The eval job depends on this, so evals never run against a broken
# MCP connection.
preflight:
name: Eval MCP preflight
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
run_install: false
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Preflight — MCP connectivity
run: cd apps/studio && pnpm evals:preflight
eval:
name: Run evals
needs: preflight
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals') && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 20
@@ -43,8 +77,8 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
+1
View File
@@ -66,6 +66,7 @@
"tailwindcss": "catalog:",
"tsconfig": "workspace:*",
"tsx": "catalog:",
"@typescript/native": "catalog:",
"typescript": "catalog:",
"unist-builder": "3.0.0"
}
@@ -20,11 +20,11 @@
--code-token-keyword: #6b35dc;
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
--code-token-constant: #15593b;
--code-token-string: #f1a10d;
--code-token-string: #c46a0a;
--code-token-comment: #7e7e7e;
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
--code-token-function: #15593b;
--code-token-string-expression: #f1a10d;
--code-token-string-expression: #c46a0a;
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
@@ -7,7 +7,7 @@ className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground
>
<AccordionItem
header={<span className="text-foreground font-medium">What you can do with the Metrics API</span>}
header="What you can do with the Metrics API"
id="how-do-i-check-when-a-user-went-through-mfa"
className="border-0 px-2 py-4"
>
@@ -25,72 +25,33 @@ using (true);`
## 1. Create a Supabase project
Before you can use Supabase, you need a Supabase project. You can create a project visually in the Dashboard or programmatically using [the Management API](/docs/reference/api/introduction).
<Tabs scrollable size="small" type="underlined" defaultActiveId="dashboard" queryGroup="create-project">
<TabPanel id="dashboard" label="Dashboard">
To start, you need a Supabase project.
Create a new Supabase project from [the Dashboard of any organization](/dashboard/new/_) you belong to.
<Admonition type="tip">
<Admonition type="tip" title="Want to create a project programmatically?">
You can also use [database.new](https://database.new) to create a new Supabase project.
Use [the Management API](/docs/reference/api/v1-create-a-project) or ask [the MCP server](/docs/guides/ai-tools/mcp#account-management) to create a new Supabase project.
</Admonition>
</TabPanel>
<TabPanel id="management-api" label="Management API">
First, get your access token from the [**Account Settings > Access Tokens**](/dashboard/account/tokens) section of the Dashboard.
Then create a project using [the Management API](/docs/reference/api/introduction):
```bash
export SUPABASE_ACCESS_TOKEN="your-access-token"
# List your organizations to get the organization ID
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
https://api.supabase.com/v1/organizations
# Create a new project (replace <org-id> with your organization ID)
curl -X POST https://api.supabase.com/v1/projects \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"organization_id": "<org-id>",
"name": "My Project",
"region": "us-east-1",
"db_pass": "<your-secure-password>"
}'
```
</TabPanel>
</Tabs>
## 2. Install agent skills (optional)
[Agent Skills](/docs/guides/ai-tools/ai-skills) are curated instruction sets that give your AI agent procedural knowledge about working with Supabase.
Install them in your project with:
```bash
npx skills add supabase/agent-skills
```
## 3. Set up your database
## 2. Set up your database
When your Supabase project is up and running, create an `instruments` table with some sample data. Then set only the privileges each Postgres role needs, add [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for enhanced security for database data by default, and create an RLS policy to make the data in the table publicly readable.
Do these steps within your project's dashboard by copying and running the snippet in your project's [SQL Editor](/dashboard/project/_/sql/new).
<Admonition type="tip">
Save some steps by <a href={`/dashboard/project/_/sql/new?content=${encodeURIComponent(sqlSetup)}`}>clicking here to prefill the SQL</a> in the SQL Editor, and then clicking **Run**.
</Admonition>
Or do this manually in your project's [SQL Editor](/dashboard/project/_/sql/new), by pasting the SQL below, and clicking **Run**.
<Admonition type="tip" title="Want to setup the database programmatically?">
You can use [the Management API](/docs/reference/api/v1-run-a-query) or ask [the MCP server](/docs/guides/ai-tools/mcp#database) to execute SQL queries.
</Admonition>
```sql SQL_EDITOR
-- Create the table
+1 -1
View File
@@ -41,7 +41,7 @@ Embeddings compress discrete information (words & symbols) into distributed cont
The chart below plots example phrases as points. Phrases with similar meanings sit close together, and unrelated phrases sit far apart.
<img
<Image
src="/docs/img/ai/vector-similarity.png"
alt="A two-dimensional chart plotting example phrases as points, where phrases with similar meanings sit close together and unrelated phrases sit far apart."
width="640"
@@ -6,7 +6,7 @@ subtitle: 'Going to production checklist for AI applications.'
sidebar_label: 'Going to Production'
---
This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.
This guide helps you prepare your application for production. It provides actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.
See our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide for more information about engineering at scale.
@@ -72,7 +72,7 @@ The values of lists and probes directly affect accuracy and queries per second (
- Higher `probes` means that select queries will be slower, but you can achieve better accuracy.
- `lists` and `probes` are not independent. Higher `lists` means that you will have to use higher `probes` to achieve the same accuracy.
You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blogpost.
You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blog post.
The chart below shows how the IVFFlat lists count affects accuracy and queries-per-second.
+1 -1
View File
@@ -27,7 +27,7 @@ Start by visiting [colab.research.google.com](https://colab.research.google.com/
We'll use the Supabase Vector client, [Vecs](/docs/guides/ai/vecs-python-client), to manage our collections.
At the top of the notebook add the notebook paste the following code and hit the "execute" button (`ctrl+enter`):
At the top of the notebook, paste the following code and click "Execute" (`ctrl+enter`):
```py
pip install vecs
+1 -1
View File
@@ -149,7 +149,7 @@ curl --output result.jpg --location --request POST 'http://localhost:54321/funct
In this example, your generated image will save to `result.jpg`:
<img
<Image
src="/docs/img/ai/hugging-face/llama-sunglasses-example.png"
alt="Llama wearing sunglasses example"
width="400"
+2 -2
View File
@@ -9,7 +9,7 @@ sidebar_label: 'LangChain'
## Initializing your database
Prepare you database with the relevant tables:
Prepare your database with the relevant tables:
<Tabs
scrollable
@@ -220,7 +220,7 @@ export const run = async () => {
LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).
You can install the LangChain Hybrid Search function though our [database.dev package manager](https://database.dev/langchain/hybrid_search).
You can install the LangChain Hybrid Search function through our [database.dev package manager](https://database.dev/langchain/hybrid_search).
## Resources
@@ -1,12 +1,12 @@
---
id: 'ai-rag-with-permissions'
title: 'RAG with Permissions'
subtitle: 'Fine-grain access control with Retrieval Augmented Generation.'
description: 'Implement fine-grain access control with retrieval augmented generation'
subtitle: 'Fine-grained access control with Retrieval Augmented Generation.'
description: 'Implement fine-grained access control with retrieval augmented generation'
sidebar_label: 'RAG with Permissions'
---
Since pgvector is built on top of Postgres, you can implement fine-grain access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.
Since pgvector is built on top of Postgres, you can implement fine-grained access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase.
Use this guide to learn how to restrict access to documents when performing retrieval augmented generation (RAG).
@@ -26,7 +26,7 @@ Embeddings are generated using a language model, and embeddings are compared to
## Embedding models
There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
There are many embedding models available today. Supabase Edge Functions has [built-in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results.
@@ -65,7 +65,7 @@ For more details on vector columns, including how to generate embeddings and sto
### Similarity metric
`pgvector` support 3 operators for computing distance between embeddings:
`pgvector` supports 3 operators for computing distance between embeddings:
| **Operator** | **Description** |
| ------------ | ---------------------- |
@@ -143,7 +143,7 @@ You can also call this method directly from SQL:
select *
from match_documents(
'[...]'::extensions.vector(512), -- pass the query embedding
0.78, -- chose an appropriate threshold for your data
0.78, -- choose an appropriate threshold for your data
10 -- choose the number of matches
);
```
@@ -5,7 +5,7 @@ description: 'Learn how to use vectors within your own Postgres tables'
sidebar_label: 'Vector columns'
---
Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section.
Supabase offers a number of different ways to store and query vectors within Postgres. The SQL included in this guide is applicable for clients in all programming languages. If you are a Python user, see your [Python client options](/docs/guides/ai/python-clients) after reading the `Learn` section.
Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvector/), a Postgres extension for storing and querying vectors in Postgres. It can be used to store [embeddings](/docs/guides/ai/concepts#what-are-embeddings).
@@ -48,7 +48,7 @@ To disable an extension, call `drop extension`.
### Create a table to store vectors
After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parenthesis) represents the number of dimensions stored in that vector.
After enabling the `vector` extension, you will get access to a new data type called `vector`. The size of the vector (indicated in parentheses) represents the number of dimensions stored in that vector.
```sql
create table documents (
@@ -100,7 +100,7 @@ This example uses the JavaScript Supabase client, but you can modify it to work
### Querying a vector / embedding
Similarity search is the most common use case for vectors. `pgvector` support 3 new operators for computing distance:
Similarity search is the most common use case for vectors. `pgvector` supports 3 new operators for computing distance:
| Operator | Description |
| -------- | ---------------------- |
+1 -1
View File
@@ -307,7 +307,7 @@ If your application uses the Supabase Database, Storage or Edge Functions, Row L
>
<AccordionItem
header={<span className="text-foreground">How do I check when a user went through MFA?</span>}
header="How do I check when a user went through MFA?"
id="how-do-i-check-when-a-user-went-through-mfa"
>
@@ -200,7 +200,7 @@ The Proxy is responsible for:
<Accordion>
<AccordionItem
header={<span className="text-foreground">What does the `cookies` object do?</span>}
header="What does the `cookies` object do?"
id="utility-cookies"
>
@@ -213,7 +213,7 @@ The Proxy is responsible for:
</AccordionItem>
<AccordionItem
header={<span className="text-foreground">Do I need to create a new client for every route?</span>}
header="Do I need to create a new client for every route?"
id="client-deduplication"
>
@@ -452,6 +452,12 @@ create table private.salaries (
);
```
<Admonition type="tip">
If you want to access a custom schema through the Supabase Data API, you need to expose it and grant the appropriate permissions. See [Using Custom Schemas](/docs/guides/api/using-custom-schemas) for detailed steps. For security best practices around schema exposure, see [Securing your API](/docs/guides/api/securing-your-api).
</Admonition>
## Views
A View is a convenient shortcut to a query. Creating a view does not involve new tables or data. When run, an underlying query is executed, returning its results to the user.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create an Astro app
## 3. Create an Astro app
Create an Astro app using the `npm create` command.
@@ -15,6 +15,16 @@ npm create astro@latest my-app
cd my-app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install Supabase client library and Node adapter
Install the `supabase-js` client library and the `@astrojs/node` adapter to enable server-side rendering.
@@ -39,11 +49,9 @@ export default defineConfig({
})
```
<$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} />
## 7. Declare Supabase environment variables
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=astro&tab=frameworks):
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=astro&tab=frameworks):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=astro">
@@ -56,6 +64,8 @@ PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "astro", "tab": "frameworks" }} />
## 8. Create a Supabase client helper
Create a utility file to initialize the Supabase client:
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create an Expo app
## 3. Create an Expo app
Create a minimal Expo app using the `create-expo-app` command with the blank TypeScript template.
@@ -14,6 +14,16 @@ Create a minimal Expo app using the `create-expo-app` command with the blank Typ
npx create-expo-app my-app --template blank-typescript
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `@supabase/supabase-js` client library which provides a convenient interface for working with Supabase from a React Native app.
@@ -24,11 +34,9 @@ Navigate to the Expo app and install `supabase-js` along with the required depen
cd my-app && npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite
```
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
## 6. Declare Supabase environment variables
Create a `.env` file in the root of your project and populate it with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true).
Create a `.env` file in the root of your project and populate it with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true).
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
@@ -41,6 +49,8 @@ EXPO_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
## 7. Initialize the Supabase client
Create a helper file at `lib/supabase.ts` to initialize the Supabase client using the environment variables.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Python app with Flask
## 3. Create a Python app with Flask
Create a new directory for your Python app and set up a virtual environment.
@@ -16,6 +16,16 @@ python3 -m venv venv
source venv/bin/activate
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install Flask and the Supabase client library
The fastest way to get started is to use Flask for the web framework and the `supabase-py` client library which provides a convenient interface for working with Supabase from a Python app.
@@ -26,11 +36,9 @@ Install both packages using pip.
pip install flask supabase
```
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
## 6. Create environment variables file
Create a `.env` file in your project root and populate it with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
Create a `.env` file in your project root and populate it with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
@@ -41,6 +49,8 @@ SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
## 7. Query data from the app
Install the `python-dotenv` package to load environment variables:
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Flutter app
## 3. Create a Flutter app
Create a Flutter app using the `flutter create` command.
@@ -14,6 +14,16 @@ Create a Flutter app using the `flutter create` command.
flutter create my_app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the [`supabase_flutter`](https://pub.dev/packages/supabase_flutter) client library which provides a convenient interface for working with Supabase from a Flutter app.
@@ -24,11 +34,9 @@ Open the `pubspec.yaml` file inside your Flutter app and add `supabase_flutter`
supabase_flutter: ^2.0.0
```
<$Partial path="api_settings.mdx" variables={{ "framework": "flutter", "tab": "mobiles" }} />
## 6. Initialize the Supabase client
Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and publishable key, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=flutter&tab=mobiles):
Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=flutter&tab=mobiles):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=mobiles&framework=flutter">
@@ -50,6 +58,8 @@ Future<void> main() async {
}
```
<$Partial path="api_settings.mdx" variables={{ "framework": "flutter", "tab": "mobiles" }} />
## 7. Query data from the app
Use a `FutureBuilder` to fetch the data when the home page loads and display the query result in a `ListView`.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Hono app
## 3. Create a Hono app
Bootstrap the Hono example app from the Supabase Samples using the CLI.
@@ -14,6 +14,16 @@ Bootstrap the Hono example app from the Supabase Samples using the CLI.
npx supabase@latest bootstrap hono
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The `package.json` file in the project includes the necessary dependencies, including `@supabase/supabase-js` and `@supabase/ssr` to help with server-side auth.
@@ -22,12 +32,9 @@ The `package.json` file in the project includes the necessary dependencies, incl
npm install
```
{/* TODO: Not ideal for frameworks that have no entry in Connect */}
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "frameworks" }} />
## 6. Set up the required environment variables
Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and publishable key, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&tab=frameworks).
Copy the `.env.example` file to `.env` and update the values with your Supabase project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&tab=frameworks).
Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings.
@@ -35,6 +42,9 @@ Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the
cp .env.example .env
```
{/* TODO: Not ideal for frameworks that have no entry in Connect */}
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "frameworks" }} />
## 7. Start the app
Start the app, go to http://localhost:5173.
@@ -6,10 +6,20 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create an iOS SwiftUI app with Xcode
## 3. Create an iOS SwiftUI app with Xcode
Select the **Xcode > New Project > iOS > App** menu item.
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
Add the [supabase-swift](https://github.com/supabase/supabase-swift) package to your app using the Swift Package Manager.
@@ -18,11 +28,9 @@ In Xcode, navigate to **File > Add Package Dependencies...** and enter the repos
Make sure to add `Supabase` product package as a dependency to your application target.
<$Partial path="api_settings.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} />
## 6. Initialize the Supabase client
Create a new `Supabase.swift` file add a new Supabase instance using your project URL and publishable key, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=swift&tab=mobiles):
Create a new `Supabase.swift` file add a new Supabase instance using your project URL and publishable key, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=swift&tab=mobiles):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=mobiles&framework=swift">
@@ -39,6 +47,8 @@ let supabase = SupabaseClient(
)
```
<$Partial path="api_settings.mdx" variables={{ "framework": "swift", "tab": "mobiles" }} />
## 7. Create a data model for instruments
Create a decodable struct to deserialize the data from the database.
@@ -6,10 +6,20 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create an Android app with Android Studio
## 3. Create an Android app with Android Studio
Select the **Android Studio > New > New Android Project** menu item.
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install dependencies
Open `build.gradle.kts` (app) file and add the serialization plugin, Ktor client, and Supabase client.
@@ -46,15 +56,13 @@ Add the following line to the `AndroidManifest.xml` file under the `manifest` ta
...
```
<$Partial path="api_settings.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} />
## 7. Initialize the Supabase client
You can create a Supabase client whenever you need to perform an API call.
For a quick example, create a client at the top of the `MainActivity.kt` file below the imports.
Replace the `supabaseUrl` and `supabaseKey` with your own, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=androidkotlin&tab=mobiles):
Replace the `supabaseUrl` and `supabaseKey` with your own, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=androidkotlin&tab=mobiles):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=mobiles&framework=androidkotlin">
@@ -74,6 +82,8 @@ val supabase = createSupabaseClient(
...
```
<$Partial path="api_settings.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} />
## 8. Create a data model for instruments
Create a serializable data class to represent the data from the database.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Laravel project
## 3. Create a Laravel project
Make sure your PHP and Composer versions are up to date, then use `composer create-project` to scaffold a new Laravel project.
@@ -16,6 +16,16 @@ See the [Laravel docs](https://laravel.com/docs/10.x/installation#creating-a-lar
composer create-project laravel/laravel example-app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the authentication template
Install [Laravel Breeze](https://laravel.com/docs/10.x/starter-kits#laravel-breeze), a basic implementation of all of Laravel's [authentication features](https://laravel.com/docs/10.x/authentication).
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Next.js app
## 3. Create a Next.js app
Use the `create-next-app` command and the `with-supabase` template, to create a Next.js app pre-configured with [Cookie-based Auth](/docs/guides/auth/server-side/creating-a-client?queryGroups=package-manager&package-manager=npm&queryGroups=framework&framework=nextjs&queryGroups=environment&environment=server), [TypeScript](https://www.typescriptlang.org/), and [Tailwind CSS](https://tailwindcss.com/).
@@ -14,11 +14,19 @@ Use the `create-next-app` command and the `with-supabase` template, to create a
npx create-next-app -e with-supabase
```
<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} />
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Declare Supabase environment variables
Rename `.env.example` to `.env.local` and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nextjs&tab=frameworks).
Rename `.env.example` to `.env.local` and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nextjs&tab=frameworks).
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs">
@@ -31,6 +39,8 @@ NEXT_PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "nextjs", "tab": "frameworks" }} />
## 6. Query Supabase data from Next.js
Create a new file at `app/instruments/page.tsx` and populate with the following.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Nuxt app
## 3. Create a Nuxt app
Create a Nuxt app using the `npx nuxi` command.
@@ -14,6 +14,16 @@ Create a Nuxt app using the `npx nuxi` command.
npx nuxi@latest init my-app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Nuxt app.
@@ -24,11 +34,9 @@ Navigate to the Nuxt app and install `supabase-js`.
cd my-app && npm install @supabase/supabase-js
```
<$Partial path="api_settings.mdx" variables={{ "framework": "nuxt", "tab": "frameworks" }} />
## 6. Declare Supabase environment variables
Create a `.env` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nuxt&tab=frameworks):
Create a `.env` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=nuxt&tab=frameworks):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nuxt">
@@ -56,6 +64,8 @@ export default defineNuxtConfig({
</$CodeTabs>
<$Partial path="api_settings.mdx" variables={{ "framework": "nuxt", "tab": "frameworks" }} />
## 7. Query data from the app
In `app.vue`, create a Supabase client using your config values and replace the existing content with the following code.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a React app
## 3. Create a React app
Create a React app using a [Vite](https://vitejs.dev/guide/) template.
@@ -14,6 +14,16 @@ Create a React app using a [Vite](https://vitejs.dev/guide/) template.
npm create vite@latest my-app -- --template react
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `supabase-js` client library, which provides a convenient interface for working with Supabase from a React app.
@@ -24,11 +34,9 @@ Navigate to the React app and install `supabase-js`.
cd my-app && npm install @supabase/supabase-js
```
<$Partial path="api_settings.mdx" variables={{ "framework": "react", "tab": "frameworks" }} />
## 6. Declare Supabase environment variables
Create a `.env.local` file and populate it with your Supabase URL and publishable key that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=react&tab=frameworks)
Create a `.env.local` file and populate it with your Supabase URL and publishable key that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=react&tab=frameworks)
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react">
@@ -41,6 +49,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "react", "tab": "frameworks" }} />
## 7. Query data from the app
Replace the contents of `App.jsx` with a `getInstruments` function that fetches the data and displays the query result on the page using a Supabase client.
@@ -24,7 +24,7 @@ To do this, set the connection mode to `Transaction` in the [Database Settings p
To get the Session mode connection pooler string, change the port of the connection string from the dashboard to 5432.
You will need the Transaction mode connection string and the Session mode connection string to set up environment variables in Step 4.
You will need the Transaction mode connection string and the Session mode connection string to set up environment variables in Step 6.
<Admonition type="tip">
@@ -50,7 +50,21 @@ While TypeScript is recommended, If you want a JavaScript app, omit the `--ts` f
yarn create redwood-app my-app --ts
```
## 4. Configure environment variables
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install MCP server (optional)
The Supabase MCP server connects AI assistants to Supabase, allowing you to interact with your projects on your behalf. Find out more on how to add it to your client in [the MCP docs](/docs/guides/ai-tools/mcp).
## 6. Configure environment variables
In your `.env` file, add the following environment variables for your database connection:
@@ -66,11 +80,11 @@ DATABASE_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabas
DIRECT_URL="postgres://postgres.[project-ref]:[db-password]@xxx.pooler.supabase.com:5432/postgres"
```
## 5. Update your Prisma schema
## 7. Update your Prisma schema
By default, RedwoodJS ships with a SQLite database, but we want to use Postgres.
Update your Prisma schema file `api/db/schema.prisma` to use your Supabase Postgres database connection environment variables you set up in Step 4.
Update your Prisma schema file `api/db/schema.prisma` to use your Supabase Postgres database connection environment variables you set up in Step 6.
```prisma name=api/db/schema.prisma
datasource db {
@@ -80,7 +94,7 @@ datasource db {
}
```
## 6. Create the instrument model and apply a schema migration
## 8. Create the instrument model and apply a schema migration
Create the Instrument model in `api/db/schema.prisma` and then run `yarn rw prisma migrate dev` from your terminal to apply the migration.
@@ -91,7 +105,7 @@ model Instrument {
}
```
## 7. Update seed script
## 9. Update seed script
Seed the database with a few instruments.
@@ -120,7 +134,7 @@ export default async () => {
}
```
## 8. Seed your database
## 10. Seed your database
Run the seed database command to populate the `Instrument` table with the instruments you created.
@@ -134,7 +148,7 @@ The reset database command `yarn rw prisma db reset` recreates the tables and al
yarn rw prisma db seed
```
## 9. Scaffold the instrument UI
## 11. Scaffold the instrument UI
Use RedwoodJS generators to scaffold a CRUD UI for the `Instrument` model.
@@ -142,13 +156,13 @@ Use RedwoodJS generators to scaffold a CRUD UI for the `Instrument` model.
yarn rw g scaffold instrument
```
## 10. Start the app
## 12. Start the app
Start the app via `yarn rw dev`. A browser will open to the RedwoodJS Splash page.
![RedwoodJS Splash Page](/docs/img/redwoodjs-qs-splash.png)
## 11. View instruments UI
## 13. View instruments UI
Click on `/instruments` to visit http://localhost:8910/instruments where should see the list of instruments.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Refine app
## 3. Create a Refine app
Create a [Refine](https://github.com/refinedev/refine) app using the [create refine-app](https://refine.dev/docs/getting-started/quickstart/).
@@ -18,11 +18,19 @@ npm create refine-app@latest -- --preset refine-supabase my-app
![Refine welcome page](/docs/img/refine-qs-welcome-page.png)
<$Partial path="api_settings.mdx" variables={{ "framework": "refine", "tab": "frameworks" }} />
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Update `supabaseClient` with environment variables
Update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API, which you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=refine&tab=frameworks). The `supabaseClient` is used in auth provider and data provider methods that allow the Refine app to connect to your Supabase backend.
Update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API, which you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=refine&tab=frameworks). The `supabaseClient` is used in auth provider and data provider methods that allow the Refine app to connect to your Supabase backend.
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=refine">
@@ -46,6 +54,8 @@ export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, {
})
```
<$Partial path="api_settings.mdx" variables={{ "framework": "refine", "tab": "frameworks" }} />
## 6. Add instruments resource and pages
Use the following code to automatically add resources and generate code for the pages to show the `instruments` data using Refine Inferencer.
@@ -16,7 +16,19 @@ Check the [Rails docs](https://guides.rubyonrails.org/getting_started.html) for
rails new blog -d=postgresql
```
## 2. Set up the Postgres connection details
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 3. Install MCP server (optional)
The Supabase MCP server connects AI assistants to Supabase, allowing you to interact with your projects on your behalf. Find out more on how to add it to your client in [the MCP docs](/docs/guides/ai-tools/mcp).
## 4. Set up the Postgres connection details
Go to [database.new](https://database.new) and create a new Supabase project. Save your database password securely.
@@ -34,7 +46,7 @@ If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/
export DATABASE_URL=postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:5432/postgres
```
## 3. Create and run a database migration
## 5. Create and run a database migration
Rails includes Active Record as the ORM as well as database migration tooling which generates the SQL migration files for you.
@@ -45,7 +57,7 @@ bin/rails generate model Article title:string body:text
bin/rails db:migrate
```
## 4. Use the model to interact with the database
## 6. Use the model to interact with the database
You can use the included Rails console to interact with the database. For example, you can create new entries or list all entries in a Model's table.
@@ -60,7 +72,7 @@ article.save # Saves the entry to the database
Article.all
```
## 5. Start the app
## 7. Start the app
Run the development server. Go to http://127.0.0.1:3000 in a browser to see your application running.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a SolidJS app
## 3. Create a SolidJS app
Create a SolidJS app using the `degit` command.
@@ -14,6 +14,16 @@ Create a SolidJS app using the `degit` command.
npx degit solidjs/templates/js my-app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SolidJS app.
@@ -24,11 +34,9 @@ Navigate to the SolidJS app and install `supabase-js`.
cd my-app && npm install @supabase/supabase-js
```
<$Partial path="api_settings.mdx" variables={{ "framework": "solidjs", "tab": "frameworks" }} />
## 6. Declare Supabase environment variables
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=solidjs&tab=frameworks):
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=solidjs&tab=frameworks):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=solidjs">
@@ -41,6 +49,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "solidjs", "tab": "frameworks" }} />
## 7. Query data from the app
In `App.jsx`, create a Supabase client to fetch the instruments data.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a SvelteKit app
## 3. Create a SvelteKit app
Create a SvelteKit app using the `npm create` command.
@@ -14,6 +14,16 @@ Create a SvelteKit app using the `npm create` command.
npx sv create my-app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a SvelteKit app.
@@ -24,11 +34,9 @@ Navigate to the SvelteKit app and install `supabase-js`.
cd my-app && npm install @supabase/supabase-js
```
<$Partial path="api_settings.mdx" variables={{ "framework": "sveltekit", "tab": "frameworks" }} />
## 6. Declare Supabase environment variables
Create a `.env` file at the root of your project and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=sveltekit&tab=frameworks):
Create a `.env` file at the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=sveltekit&tab=frameworks):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=sveltekit">
@@ -41,6 +49,8 @@ PUBLIC_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
PUBLIC_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "sveltekit", "tab": "frameworks" }} />
## 7. Create the Supabase client
Create a `src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client:
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a TanStack Start app
## 3. Create a TanStack Start app
Create a TanStack Start app using the official CLI.
@@ -14,6 +14,16 @@ Create a TanStack Start app using the official CLI.
npm create @tanstack/start@latest my-app -- --package-manager npm --toolchain biome
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a TanStack Start app.
@@ -24,11 +34,9 @@ Navigate to the TanStack Start app and install `supabase-js`.
cd my-app && npm install @supabase/supabase-js
```
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
## 6. Declare Supabase environment variables
Create a `.env` file in the root of your project and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
Create a `.env` file in the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
@@ -39,6 +47,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} />
## 7. Create a Supabase client utility
Create a new file at `src/utils/supabase.ts` to initialize the Supabase client.
@@ -6,7 +6,7 @@ breadcrumb: 'Framework Quickstarts'
<$Partial path="quickstart_db_setup.mdx" />
## 4. Create a Vue app
## 3. Create a Vue app
Create a Vue app using the `npm init` command.
@@ -14,6 +14,16 @@ Create a Vue app using the `npm init` command.
npm init vue@latest my-app
```
## 4. Install Agent Skills (optional)
Supabase's [Agent Skills](/docs/guides/ai-tools/ai-skills) is a curated set of instructions that give your AI agent procedural knowledge about working with Supabase.
To install, run the following command in the root of your project:
```bash
npx skills add supabase/agent-skills
```
## 5. Install the Supabase client library
The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a Vue app.
@@ -24,11 +34,9 @@ Navigate to the Vue app and install `supabase-js`.
cd my-app && npm install @supabase/supabase-js
```
<$Partial path="api_settings.mdx" variables={{ "framework": "vue", "tab": "frameworks" }} />
## 6. Declare Supabase environment variables
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper above, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=vue&tab=frameworks):
Create a `.env.local` file and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&framework=vue&tab=frameworks):
<Button variant="primary" asChild>
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=vue">
@@ -41,6 +49,8 @@ VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_SUPABASE_PUBLISHABLE_KEY>
```
<$Partial path="api_settings.mdx" variables={{ "framework": "vue", "tab": "frameworks" }} />
## 7. Create the Supabase client
Create a `/src/lib` directory in your Vue app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client:
@@ -58,7 +58,7 @@ and run one of the following:
- `sudo rpm -i <...>.rpm`
</TabPanel>
<TabPanel id="npm" label="nodejs">
<TabPanel id="npm" label="Node.js">
Run the CLI by prefixing each command with `npx` or `bunx`:
@@ -122,7 +122,7 @@ brew link --overwrite supabase-beta
Beta builds are attached to [GitHub pre-releases](https://github.com/supabase/cli/releases). Download the `.apk`, `.deb`, or `.rpm` for your platform and install with the same commands as [Linux packages](#linux-packages) above.
</TabPanel>
<TabPanel id="npm" label="nodejs">
<TabPanel id="npm" label="Node.js">
Install as a dev dependency:
@@ -199,7 +199,7 @@ brew upgrade supabase-beta
- `sudo rpm -i <...>.rpm`
</TabPanel>
<TabPanel id="npm" label="nodejs">
<TabPanel id="npm" label="Node.js">
If you have installed the CLI as dev dependency via [npm](https://www.npmjs.com/package/supabase), you can update it with:
@@ -80,6 +80,7 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -100,6 +101,7 @@ Migrate existing users to Supabase Auth. This requires two main steps: first, ch
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -122,6 +124,7 @@ For passwordless signin via email or phone, check for users with verified email
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -162,6 +165,7 @@ Both columns are accessible from the admin user methods. To create a user with c
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')
// ---cut---
@@ -205,7 +209,7 @@ create table private.user_metadata (
>
<AccordionItem
header={<span className="text-foreground">I have IDs assigned to existing users in my database, how can I maintain these IDs?</span>}
header="I have IDs assigned to existing users in my database, how can I maintain these IDs?"
id="custom-user-id"
>
@@ -228,7 +232,7 @@ const { data, error } = await supabase.auth.admin.createUser({
</AccordionItem>
<AccordionItem
header={<span className="text-foreground">How can I allow my users to retain their existing password?</span>}
header="How can I allow my users to retain their existing password?"
id="existing-password"
>
@@ -237,7 +241,7 @@ Supabase Auth never stores passwords as plaintext. Since Supabase Auth supports
</AccordionItem>
<AccordionItem
header={<span className="text-foreground">My users have multi-factor authentication (MFA) enabled, how do I make sure they don't have to set up MFA again?</span>}
header="My users have multi-factor authentication (MFA) enabled, how do I make sure they don't have to set up MFA again?"
id="mfa"
>
@@ -246,7 +250,7 @@ You can obtain an export of your users' MFA secrets by opening a support ticket
</AccordionItem>
<AccordionItem
header={<span className="text-foreground">How do I migrate existing SAML Single Sign-On (SSO) connections?</span>}
header="How do I migrate existing SAML Single Sign-On (SSO) connections?"
id="saml"
>
@@ -255,7 +259,7 @@ Customers may need to link their identity provider with Supabase Auth separately
</AccordionItem>
<AccordionItem
header={<span className="text-foreground">How do I migrate my Auth0 organizations to Supabase?</span>}
header="How do I migrate my Auth0 organizations to Supabase?"
id="migrate-org"
>
@@ -72,7 +72,12 @@ print('Analytics bucket created:', response)
4. Select **Analytics Bucket** as the bucket type.
5. Click **Create**.
<img alt="Create Analytics Bucket in Dashboard" src="/docs/img/storage/iceberg-bucket.png" />
<Image
alt="Create Analytics Bucket in Dashboard"
width="644"
height="402"
src="/docs/img/storage/iceberg-bucket.png"
/>
## Next steps
@@ -23,7 +23,12 @@ To authenticate with S3, generate a pair of credentials (Access Key ID and Secre
This is all the information you need to connect to Supabase Storage using any S3-compatible service.
<img alt="Storage S3 Access keys" src="/docs/img/storage/s3-credentials.png" width="100%" />
<Image
alt="Storage S3 Access keys"
src="/docs/img/storage/s3-credentials.png"
width="749"
height="490"
/>
<Admonition type="note">
@@ -27,10 +27,11 @@ Pick the workflow that best matches your tooling. Cards link to Supabase-authore
<MetricsStackCards />
<img
<Image
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
alt="Supabase Grafana dashboard showcasing database metrics"
className="mt-8 rounded-lg border border-foreground/10 shadow-xs"
width="3000"
height="1509"
/>
## Additional resources
@@ -44,10 +44,11 @@ If you prefer to reuse an existing Grafana Agent deployment, configure an [integ
This dashboard includes 200+ charts grouped by CPU, IO, connections, replication, WAL, and bloat indicators.
<img
<Image
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
alt="Supabase Grafana dashboard showcasing database metrics"
className="mt-6 rounded-lg border border-foreground/10 shadow-xs"
width="3000"
height="1509"
/>
## 4. Configure alerts (optional)
@@ -63,10 +63,11 @@ Install Grafana (Docker image, Helm chart, or packages) and connect it to Promet
You now have over 200 production-ready panels covering CPU, IO, WAL, replication, index bloat, and query throughput.
<img
<Image
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
alt="Supabase Grafana dashboard showcasing database metrics"
className="mt-6 rounded-lg border border-foreground/10 shadow-xs"
width="3000"
height="1509"
/>
## 4. Configure alerting
@@ -51,10 +51,11 @@ No matter which collector you use, you need to hit the Metrics API once per minu
- For other tools, group metrics by categories (CPU, IO, WAL, replication, connections) and recreate the visualizations that matter most to your team.
- Tag or relabel series with `project`, `env`, or `team` labels to make multi-project views easier.
<img
<Image
src="/docs/img/guides/platform/supabase-grafana-prometheus.png"
alt="Supabase Grafana dashboard showcasing database metrics"
className="mt-6 rounded-lg border border-foreground/10 shadow-xs"
width="3000"
height="1509"
/>
## 4. Alerts and automation
@@ -3,121 +3,82 @@ title: 'Sentry integration'
subtitle: 'Integrate Sentry to monitor errors from a Supabase client'
---
You can use [Sentry](https://sentry.io/welcome/) to monitor errors thrown from a Supabase JavaScript client. Install the [Supabase Sentry integration](https://github.com/supabase-community/sentry-integration-js) to get started.
You can use [Sentry](https://sentry.io/welcome/) to monitor errors thrown from a Supabase JavaScript client. Support for Supabase is built directly into the Sentry JavaScript SDK.
The Sentry integration supports browser, Node, and edge environments.
## Installation
Install the Sentry integration using your package manager:
<Tabs scrollable queryGroup="package-manager" defaultActiveId="npm" type="underlined" size="small" >
<TabPanel id="npm" label="npm">
```sh
npm install @supabase/sentry-js-integration
```
</TabPanel>
<TabPanel id="yarn" label="yarn">
```sh
yarn add @supabase/sentry-js-integration
```
</TabPanel>
<TabPanel id="pnpm" label="pnpm">
```sh
pnpm add @supabase/sentry-js-integration
```
</TabPanel>
</Tabs>
## Use
The integration instruments database queries and authentication calls made through `supabase-js`, creating spans for performance monitoring and capturing errors. It supports browser, Node, and edge environments.
<Admonition type='note'>
If you are using Sentry JavaScript SDK v7, reference [`supabase-community/sentry-integration-js` repository](https://github.com/supabase-community/sentry-integration-js/blob/master/README-v7.md) instead.
The built-in integration requires Sentry JavaScript SDK **v9.14.0 or later**. If you're on an older SDK (including v7), use the community [`@supabase/sentry-js-integration`](https://github.com/supabase-community/sentry-integration-js) package instead, which the built-in integration is based on.
</Admonition>
To use the Supabase Sentry integration, add it to your `integrations` list when initializing your Sentry client.
## Use
You can supply either the Supabase Client constructor or an already-initiated instance of a Supabase Client.
There are two ways to enable the integration. Both take an initialized Supabase client instance.
<Tabs scrollable defaultActiveId="constructor" type="underlined" size="small">
<Tabs scrollable defaultActiveId="integration" type="underlined" size="small">
<TabPanel id="constructor" label="With constructor">
<TabPanel id="integration" label="Via Sentry.init">
```ts
import * as Sentry from '@sentry/browser'
import { SupabaseClient } from '@supabase/supabase-js'
import { supabaseIntegration } from '@supabase/sentry-js-integration'
Sentry.init({
dsn: SENTRY_DSN,
integrations: [
supabaseIntegration(SupabaseClient, Sentry, {
tracing: true,
breadcrumbs: true,
errors: true,
}),
],
})
```
</TabPanel>
<TabPanel id="instance" label="With instance">
Add `supabaseIntegration` to the `integrations` list when you initialize Sentry. Use this when your `Sentry.init` call and your Supabase client live in the same place.
```ts
import * as Sentry from '@sentry/browser'
import { createClient } from '@supabase/supabase-js'
import { supabaseIntegration } from '@supabase/sentry-js-integration'
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY)
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
integrations: [
supabaseIntegration(supabaseClient, Sentry, {
tracing: true,
breadcrumbs: true,
errors: true,
}),
Sentry.browserTracingIntegration(),
Sentry.supabaseIntegration({ supabaseClient }),
],
})
```
</TabPanel>
</Tabs>
<TabPanel id="instrument" label="Via instrumentSupabaseClient">
All available configuration options are available in our [`supabase-community/sentry-integration-js` repository](https://github.com/supabase-community/sentry-integration-js/blob/master/README.md#options).
## Deduplicating spans
If you're already monitoring HTTP errors in Sentry, for example with the HTTP, Fetch, or Undici integrations, you will get duplicate spans for Supabase calls. You can deduplicate the spans by skipping them in your other integration:
Call `Sentry.instrumentSupabaseClient` where you create the client. This is the better fit for frameworks (like Next.js) where `Sentry.init` runs in a separate config file. Instrument each client you create: it patches database calls per runtime (browser, server, edge) and auth calls per client instance, so setups that create a client per request (like `@supabase/ssr`) must instrument each one. See the Next.js example below.
```ts
import * as Sentry from '@sentry/browser'
import { SupabaseClient } from '@supabase/supabase-js'
import { supabaseIntegration } from '@supabase/sentry-js-integration'
import { createClient } from '@supabase/supabase-js'
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY)
Sentry.instrumentSupabaseClient(supabaseClient)
```
</TabPanel>
</Tabs>
<Admonition type='note'>
By default, query filters and mutation bodies are redacted from spans and breadcrumbs. To capture them, pass `sendOperationData: true` where you set up instrumentation (`Sentry.supabaseIntegration({ supabaseClient, sendOperationData: true })` or `Sentry.instrumentSupabaseClient(client, { sendOperationData: true })`), or enable `dataCollection: { userInfo: true }` in your `Sentry.init` options, which applies to every client in that runtime.
</Admonition>
## Deduplicating spans
Sentry's HTTP and Fetch tracing integrations are enabled by default in the Node and Next.js SDKs, so the underlying Supabase REST calls are traced as `http.client` spans in addition to the `db` spans from the Supabase integration. This is optional cleanup: if you'd rather not see both, skip the Supabase REST requests in your other integration.
```ts
import * as Sentry from '@sentry/browser'
import { createClient } from '@supabase/supabase-js'
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY)
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
integrations: [
supabaseIntegration(SupabaseClient, Sentry, {
tracing: true,
breadcrumbs: true,
errors: true,
}),
Sentry.supabaseIntegration({ supabaseClient }),
// @sentry/browser
Sentry.browserTracingIntegration({
@@ -126,23 +87,14 @@ Sentry.init({
},
}),
// or @sentry/node
Sentry.httpIntegration({
tracing: {
ignoreOutgoingRequests: (url) => {
return url.startsWith(`${SUPABASE_URL}/rest`)
},
},
}),
// or @sentry/node with Fetch support
// or @sentry/node (supabase-js uses fetch, so filter the Fetch integration)
Sentry.nativeNodeFetchIntegration({
ignoreOutgoingRequests: (url) => {
return url.startsWith(`${SUPABASE_URL}/rest`)
},
}),
// or @sentry/WinterCGFetch for Next.js Proxy & Edge Functions
// or @sentry/nextjs for Proxy & Edge Functions
Sentry.winterCGFetchIntegration({
breadcrumbs: true,
shouldCreateSpanForRequest: (url) => {
@@ -153,125 +105,28 @@ Sentry.init({
})
```
## Example Next.js configuration
## Configuration for Next.js
See this example for a setup with Next.js to cover browser, server, and edge environments. First, run through the [Sentry Next.js wizard](https://docs.sentry.io/platforms/javascript/guides/nextjs/#install) to generate the base Next.js configuration. Then add the Supabase Sentry Integration to all your `Sentry.init` calls with the appropriate filters.
Next.js runs Sentry across browser, server, and edge runtimes, and auth-aware setups (like `@supabase/ssr`) create a Supabase client per request. Since `instrumentSupabaseClient` patches database calls per runtime and auth calls per client instance, call it inside each of your client factories rather than on a single shared instance.
<Tabs scrollable defaultActiveId="browser" type="underlined" size="small">
1. Run through the [Sentry Next.js wizard](https://docs.sentry.io/platforms/javascript/guides/nextjs/#install) to set up the base Sentry configuration.
<TabPanel id="browser" label="Browser">
2. Add `Sentry.instrumentSupabaseClient` to each factory. For example, the server client with `@supabase/ssr`:
```ts sentry.client.config.ts
```ts utils/supabase/server.ts
import * as Sentry from '@sentry/nextjs'
import { SupabaseClient } from '@supabase/supabase-js'
import { supabaseIntegration } from '@supabase/sentry-js-integration'
import { createServerClient } from '@supabase/ssr'
Sentry.init({
dsn: SENTRY_DSN,
integrations: [
supabaseIntegration(SupabaseClient, Sentry, {
tracing: true,
breadcrumbs: true,
errors: true,
}),
Sentry.browserTracingIntegration({
shouldCreateSpanForRequest: (url) => {
return !url.startsWith(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest`)
},
}),
],
export async function createClient() {
const client = createServerClient(/* your usual URL, key, and cookie config */)
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: true,
})
```
</TabPanel>
<TabPanel id="server" label="Server">
```ts sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
import { SupabaseClient } from '@supabase/supabase-js'
import { supabaseIntegration } from '@supabase/sentry-js-integration'
Sentry.init({
dsn: SENTRY_DSN,
integrations: [
supabaseIntegration(SupabaseClient, Sentry, {
tracing: true,
breadcrumbs: true,
errors: true,
}),
Sentry.nativeNodeFetchIntegration({
breadcrumbs: true,
ignoreOutgoingRequests: (url) => {
return url.startsWith(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest`)
},
}),
],
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: true,
})
```
</TabPanel>
<TabPanel id="edge" label="Proxy & Edge">
```js sentry.edge.config.ts
import * as Sentry from '@sentry/nextjs'
import { SupabaseClient } from '@supabase/supabase-js'
import { supabaseIntegration } from '@supabase/sentry-js-integration'
Sentry.init({
dsn: SENTRY_DSN,
integrations: [
supabaseIntegration(SupabaseClient, Sentry, {
tracing: true,
breadcrumbs: true,
errors: true,
}),
Sentry.winterCGFetchIntegration({
breadcrumbs: true,
shouldCreateSpanForRequest: (url) => {
return !url.startsWith(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/rest`)
},
}),
],
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: true,
})
```
</TabPanel>
<TabPanel id="instrumentation" label="Instrumentation">
```js instrumentation.ts
// https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config')
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config')
}
Sentry.instrumentSupabaseClient(client)
return client
}
```
</TabPanel>
3. Apply the same in your browser client using (`createBrowserClient`) and middleware client so every runtime is covered.
</Tabs>
4. To include query filters and mutation bodies, enable `dataCollection: { userInfo: true }` in each runtime's Sentry config, or pass `Sentry.instrumentSupabaseClient(client, { sendOperationData: true })` at the call site.
Afterwards, build your application (`npm run build`) and start it locally (`npm run start`). You will now see the transactions being logged in the terminal when making supabase-js requests.
5. Build and run your application (`npm run build && npm run start`). Supabase queries now appear as `db` spans in your Sentry traces.
@@ -102,9 +102,9 @@ const GuideTemplate = ({
<ReactMarkdown>{meta?.title || 'Supabase Docs'}</ReactMarkdown>
</h1>
{meta?.subtitle && (
<h2 className="mt-1 text-xl text-foreground-light [&>p]:m-0">
<div className="mt-3 not-prose [&_p]:text-xl [&_p]:leading-7 text-foreground-light [&>p]:m-0">
<ReactMarkdown>{meta.subtitle}</ReactMarkdown>
</h2>
</div>
)}
</header>
@@ -121,7 +121,7 @@ const GuideTemplate = ({
className={cn(
'w-fit',
'flex items-center gap-1',
'text-sm text-scale-1000 hover:text-scale-1200',
'text-sm text-tertiary-foreground hover:text-foreground',
'transition-colors'
)}
target="_blank"
+1 -2
View File
@@ -17,8 +17,7 @@ export function GuideFooter({ className, editLink }: GuideFooterProps) {
className={cn(
'w-fit',
'flex items-center gap-1',
'text-sm text-scale-1000 hover:text-scale-1200',
'transition-colors'
'text-sm text-tertiary-foreground hover:text-foreground'
)}
target="_blank"
rel="noreferrer noopener edit"
+1 -1
View File
@@ -18,7 +18,7 @@ export function GuideHeader({ className }: GuideHeaderProps) {
<ReactMarkdown>{meta?.title || 'Supabase Docs'}</ReactMarkdown>
</h1>
{meta?.subtitle && (
<div className="mt-6 not-prose [&_p]:text-xl [&_p]:leading-7 text-foreground-light [&>p]:m-0">
<div className="mt-3 not-prose [&_p]:text-xl [&_p]:leading-7 text-foreground-light [&>p]:m-0">
<ReactMarkdown>{meta.subtitle}</ReactMarkdown>
</div>
)}
@@ -13,12 +13,15 @@ import { parse as parseToml } from 'smol-toml'
import { mcpConfigPanelMarkdown as McpConfigPanel } from 'ui-patterns/McpUrlBuilder/McpConfigPanel.md'
import { addBaseUrlPrefix } from './internal-links'
import { AccordionItem } from './markdown-schema/Accordion'
import { Admonition } from './markdown-schema/Admonition'
import { AuthProviders } from './markdown-schema/AuthProviders'
import { ComputeDiskLimitsTable } from './markdown-schema/ComputeDiskLimitsTable'
import { ContentListings } from './markdown-schema/ContentListings'
import { CustomContent } from './markdown-schema/CustomContent'
import { ErrorCodes } from './markdown-schema/ErrorCodes'
import { IconCheck, IconX } from './markdown-schema/Icons'
import { Image } from './markdown-schema/Image'
import { Link } from './markdown-schema/Link'
import { McpCiConfigBlock } from './markdown-schema/McpCiConfigBlock'
import { MetricsStackCards } from './markdown-schema/MetricsStackCards'
@@ -151,7 +154,11 @@ function applySchema(parent: Parent, schema: ComponentSchema): void {
* component not listed is unwrapped (children are kept, wrapper is dropped).
*/
const SCHEMA: ComponentSchema = {
AccordionItem,
Admonition,
IconCheck,
IconX,
Image,
AuthProviders,
ComputeDiskLimitsTable,
CustomContent,
@@ -0,0 +1,10 @@
export const AccordionItem = ({
props,
children,
}: {
props: Record<string, unknown>
children: string
}): string => {
const header = String(props.header ?? '').trim()
return header ? `**${header}**\n\n${children}` : children
}
@@ -0,0 +1,2 @@
export const IconCheck = (): string => '✅'
export const IconX = (): string => '❌'
@@ -0,0 +1,21 @@
import { getInternalLinkBaseUrl, withDocsBasePath } from '../internal-links'
export const Image = ({ props }: { props: Record<string, unknown> }): string => {
const alt = String(props.alt ?? '')
const rawSrc = props.src as string
// propsFrom() always stores attribute values as strings: either the literal
// string value for `src="/..."`, or the raw JS expression for `src={{ dark, light }}`.
const srcStr = String(rawSrc ?? '')
let src = String(props.src)
if (!srcStr.startsWith('/')) {
// Object expression form: `src={{ dark: '/...', light: '/...' }}`
const match =
srcStr.match(/dark:\s*['"]([^'"]+)['"]/) ?? srcStr.match(/light:\s*['"]([^'"]+)['"]/)
src = match ? match[1] : ''
}
const baseUrl = getInternalLinkBaseUrl()
return `![${alt}](${baseUrl}${withDocsBasePath(src)})`
}
+13 -2
View File
@@ -1,3 +1,14 @@
export const Panel = ({ props }: { props: Record<string, unknown> }): string => {
return String(props.title)
export const Panel = ({
props,
children,
}: {
props: Record<string, unknown>
children: string
}): string => {
const title = String(props.title ?? '')
const description = children.replace(/\s+/g, ' ').trim()
if (title && description) return `${title}: ${description}`
return title || description
}
+1
View File
@@ -181,6 +181,7 @@
"tsconfig": "workspace:*",
"tsx": "catalog:",
"twoslash": "^0.3.1",
"@typescript/native": "catalog:",
"typescript": "catalog:",
"unist-util-visit-parents": "5.1.3",
"vite": "catalog:",
+1
View File
@@ -264,6 +264,7 @@ Sean Romberg
Sean Thompson
Sergio Cioban Filho
Shaii O
Shane Adams
Shane E
Shreekar Shetty
Sreyas Udayavarman
+90
View File
@@ -2589,11 +2589,16 @@ functions:
notes: |
Delete a user.
- The `deleteUser()` method requires the user's ID, which maps to the `auth.users.id` column.
- When `shouldSoftDelete` is `true`, the user is soft-deleted: their record and associated data are retained but the user is marked as deleted. Defaults to `false`, which permanently removes the user.
params:
- name: id
isOptional: false
type: String
description: ID of the user to be deleted.
- name: shouldSoftDelete
isOptional: true
type: bool
description: If true, soft-deletes the user (keeps the record but marks it deleted). Defaults to false (permanent delete).
examples:
- id: removes-a-user
name: Removes a user
@@ -2603,6 +2608,16 @@ functions:
await supabase.auth.admin
.deleteUser('715ed5db-f090-4b8c-a067-640ecee36aa0');
```
- id: soft-delete-a-user
name: Soft-delete a user
code: |
```dart
await supabase.auth.admin
.deleteUser(
'715ed5db-f090-4b8c-a067-640ecee36aa0',
shouldSoftDelete: true,
);
```
- id: invite-user-by-email
title: 'inviteUserByEmail()'
notes: |
@@ -4634,6 +4649,10 @@ functions:
isOptional: false
type: int
description: The number of seconds until the signed URL expires. For example, 60 for a URL which is valid for one minute.
- name: download
isOptional: true
type: DownloadBehavior
description: Triggers the file to be downloaded rather than opened in the browser. Use `DownloadBehavior.withOriginalName` to keep the original file name or `DownloadBehavior.named('custom.png')` to override it.
- name: transform
isOptional: true
type: TransformOptions
@@ -4690,6 +4709,61 @@ functions:
```json
'https://example.supabase.co/storage/v1/object/sign/avatars/folder/avatar1.png?token=<TOKEN>''
```
- id: create-signed-url-with-download
name: With download
code: |
```dart
final String signedUrl = await supabase
.storage
.from('avatars')
.createSignedUrl(
'avatar1.png',
60,
download: DownloadBehavior.withOriginalName,
);
```
- id: from-create-signed-upload-url
description: |
Creates a signed upload URL. Signed upload URLs can be used to upload files to a bucket without further authentication. They are valid for 2 hours.
title: 'from.createSignedUploadUrl()'
notes: |
- Policy permissions required:
- `buckets` permissions: none
- `objects` permissions: `insert`
- Refer to the [Storage guide](https://supabase.com/docs/guides/storage/security/access-control) on how access control works
params:
- name: path
isOptional: false
type: String
description: The file path, including the current file name. For example folder/image.png.
- name: upsert
isOptional: true
type: bool
description: If true, the signed URL allows overwriting an existing file at the path. Defaults to false.
examples:
- id: create-signed-upload-url
name: Create signed upload URL
isSpotlight: true
code: |
```dart
final response = await supabase
.storage
.from('avatars')
.createSignedUploadUrl('folder/avatar1.png');
```
- id: create-signed-upload-url-with-upsert
name: With upsert
code: |
```dart
final response = await supabase
.storage
.from('avatars')
.createSignedUploadUrl(
'folder/avatar1.png',
upsert: true,
);
```
- id: from-get-public-url
description: |
@@ -4706,6 +4780,10 @@ functions:
isOptional: false
type: String
description: The path and name of the file to generate the public URL for. For example folder/image.png.
- name: download
isOptional: true
type: DownloadBehavior
description: Triggers the file to be downloaded rather than opened in the browser. Use `DownloadBehavior.withOriginalName` to keep the original file name or `DownloadBehavior.named('custom.png')` to override it.
- name: transform
isOptional: true
type: TransformOptions
@@ -4766,6 +4844,18 @@ functions:
```json
'https://example.supabase.co/storage/v1/object/public/public-bucket/folder/avatar1.png'
```
- id: returns-the-url-for-an-asset-with-download
name: Trigger download
code: |
```dart
final String publicUrl = supabase
.storage
.from('public-bucket')
.getPublicUrl(
'avatar1.png',
download: DownloadBehavior.withOriginalName,
);
```
- id: from-download
description: |
+25 -7
View File
@@ -4384,6 +4384,8 @@ functions:
It's best to only enable this for testing environments but if you wish to enable it for production you can provide additional protection by using a `pre-request` function.
Follow the [Performance Debugging Guide](/docs/guides/database/debugging-performance) to enable the functionality on your project.
notes: |
The `format` parameter accepts an `ExplainFormat` value — either `.text` (default, human-readable) or `.json` (machine-readable JSON).
examples:
- id: get-execution-plan
name: Get the execution plan
@@ -4463,6 +4465,21 @@ functions:
By default, the data is returned in TEXT format, but can also be returned as JSON by using the `format` parameter.
hideCodeBlock: true
isSpotlight: false
- id: get-execution-plan-json-format
name: Get the execution plan as JSON
code: |
```swift
try await supabase
.from("instruments")
.select()
.explain(format: .json)
.execute()
.value
```
description: |
Use `format: .json` to get a machine-readable JSON execution plan. The `ExplainFormat` type accepts `.text` (default) or `.json`.
isSpotlight: false
- id: invoke
title: invoke()
description: |
@@ -5176,7 +5193,7 @@ functions:
.createBucket(
"avatars",
options: BucketOptions(
public: false,
isPublic: false,
allowedMimeTypes: ["image/png"],
fileSizeLimit: 1024
)
@@ -5216,7 +5233,7 @@ functions:
.updateBucket(
"avatars",
options: BucketOptions(
public: false,
isPublic: false,
fileSizeLimit: 1024,
allowedMimeTypes: ["image/png"]
)
@@ -5375,12 +5392,13 @@ functions:
let signedURL = try await supabase.storage
.from("avatars")
.createSignedURL(
path: "folder/avatar1.png", expiresIn: 60,
download: true
path: "folder/avatar1.png",
expiresIn: 60,
download: .withOriginalName
)
```
note: |
You can also sepcify a `String` in the `download` parameter to define the file name for the downloaded asset.
Use `.withOriginalName` to download with the original file name, or `.named("custom.png")` to specify a custom file name. The `download: Bool` overload still compiles but is disfavored.
- id: from-create-signed-urls
title: from.createSignedUrls()
@@ -5519,11 +5537,11 @@ functions:
.from("public-bucket")
.getPublicURL(
path: "folder/avatar1.png",
download: true
download: .withOriginalName
)
```
note: |
You can also sepcify a `String` in the `download` parameter to define the file name for the downloaded asset.
Use `.withOriginalName` to download with the original file name, or `.named("custom.png")` to specify a custom file name. The `download: Bool` overload still compiles but is disfavored.
- id: from-download
title: from.download()
+5 -5
View File
@@ -424,7 +424,7 @@ th code {
--code-foreground: #ffffff;
--code-token-constant: #3ecf8e;
--code-token-string: #ffcda1;
--code-token-comment: #7e7e7e;
--code-token-comment: #949494;
--code-token-parameter: #ffffff;
--code-token-function: #3ecf8e;
--code-token-string-expression: #ffcda1;
@@ -436,14 +436,14 @@ th code {
}
[data-theme='light'],
.light {
--code-token-keyword: #6b35dc;
--code-token-keyword: #5f2fc4;
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
--code-token-constant: #15593b;
--code-token-string: #f1a10d;
--code-token-comment: #7e7e7e;
--code-token-string: #9a5200;
--code-token-comment: #6a6a6a;
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
--code-token-function: #15593b;
--code-token-string-expression: #f1a10d;
--code-token-string-expression: #9a5200;
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
+2 -2
View File
@@ -20,11 +20,11 @@
--code-token-keyword: #6b35dc;
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
--code-token-constant: #15593b;
--code-token-string: #f1a10d;
--code-token-string: #c46a0a;
--code-token-comment: #7e7e7e;
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
--code-token-function: #15593b;
--code-token-string-expression: #f1a10d;
--code-token-string-expression: #c46a0a;
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
+2 -2
View File
@@ -3,7 +3,7 @@
"react-hooks/exhaustive-deps": 161,
"import/no-anonymous-default-export": 57,
"@tanstack/query/exhaustive-deps": 9,
"@typescript-eslint/no-explicit-any": 898,
"@typescript-eslint/no-explicit-any": 897,
"no-restricted-imports": 0,
"no-restricted-exports": 198,
"react/no-unstable-nested-components": 38,
@@ -556,7 +556,6 @@
"hooks/misc/withAuth.tsx": 1,
"hooks/ui/useClickedOutside.ts": 2,
"hooks/ui/useFlag.ts": 1,
"instrumentation-client.ts": 3,
"lib/ai/generate-assistant-response.ts": 2,
"lib/ai/model.ts": 2,
"lib/ai/model.utils.ts": 3,
@@ -571,6 +570,7 @@
"lib/pg-format.ts": 6,
"lib/profile.tsx": 1,
"lib/role-impersonation.ts": 1,
"lib/sentry-client-options.ts": 2,
"lib/telemetry/track.ts": 1,
"pages/_app.tsx": 1,
"pages/api/ai/feedback/rate.ts": 2,
+74
View File
@@ -494,6 +494,80 @@ Keep this plugin even after migration — it's not a Next-related shim,
it's general protection against this entire class of bug. Just clear
the allowlist when the underlying cycle is gone.
### `@sentry/nextjs` → `@sentry/react` alias
`resolve.alias` in `vite.config.ts` rewrites the bare `@sentry/nextjs`
import to `compat/sentry-nextjs.ts`, which re-exports `@sentry/react`
(the same-version package `@sentry/nextjs` wraps on the client) plus
explicit stand-ins for the Next-only APIs (`captureRouterTransitionStart`,
`captureRequestError`, `withSentryConfig`).
Why: `@sentry/nextjs`'s client entry imports
`next/dist/shared/lib/constants`, whose module scope evaluates
`...(process?.features?.typescript ? ['next.config.mts'] : [])`.
Optional chaining does **not** guard an undeclared `process` identifier,
so every built client chunk containing it (table editor was the canary)
threw `ReferenceError: process is not defined` at module load. Dev was
unaffected (dev pipeline shims `process`), so it only surfaced in the
production/test build.
The alias also made the previous `@sentry/nextjs` SSR workarounds
(`ssr.noExternal` entry + `ssr.optimizeDeps.include`) obsolete — the id
is rewritten before SSR resolution, and `@sentry/react` ships real ESM.
App source keeps importing `@sentry/nextjs` so the Next build
(`build:next`) is untouched; drop the alias + shim together with the
Next build when the migration is done (switch imports to
`@sentry/react` directly).
### GraphiQL Monaco workers: `setup-workers/webpack` → `setup-workers/vite`
App source (`GraphiQLTab.tsx`) imports `graphiql/setup-workers/webpack`,
which registers `MonacoEnvironment.getWorker` using
`new Worker(new URL('monaco-editor/...', import.meta.url))` — the URL form
webpack/turbopack rewrites at build time. Vite doesn't rewrite bare module
specifiers inside `new URL(..., import.meta.url)`, so under the TanStack
build the worker URLs 404'd and Monaco fell back to running the `json`,
`editorWorkerService` and `graphql` workers on the main thread ("Could not
create web worker(s). Falling back to loading web worker code in main
thread" in the console).
The `graphiqlViteWorkers` plugin in `vite.config.ts` resolves that import
to graphiql's own `setup-workers/vite` variant (same three workers via
Vite `?worker` imports) in client builds; SSR resolution is untouched. The
import specifier stays `.../webpack` in app source so the Next build keeps
working. The whole setup-workers chain is also in `optimizeDeps.exclude` —
the Rolldown dep optimizer can't load `?worker` ids
(`UNLOADABLE_DEPENDENCY`), so the modules go through the normal transform
pipeline where Vite's built-in worker plugin handles them. Drop the plugin
and the exclude, and switch the import to `graphiql/setup-workers/vite`,
when the Next build goes away.
### Raw-text imports: `*.md` + `public/deno/*.d.ts` (`rawTextLoader`)
Next's raw-loader rules (next.config.ts `turbopack.rules`) serve `*.md`
files and the Deno typings `public/deno/edge-runtime.d.ts` /
`public/deno/lib.deno.d.ts` as JS modules whose default export is the
file's text. The `rawTextLoader` plugin in `vite.config.ts` mirrors that
for the Vite pipeline:
- `*.md` — plain `transform` (used by
`static-data/integrations/*/overview.md` via the literal-import registry
in `static-data/integrations/overviews.ts`).
- The two Deno `.d.ts` files (used by `components/ui/AIEditor` as Monaco
extra libs for edge-function editors) — an exact-specifier allowlist
resolved to `\0`-virtual ids and served from a `load` hook. They can't go
through `transform`: Rolldown's native dep scanner skips JS plugin hooks
and hard-fails parsing TS _declaration_ syntax (`get stdin(): ...;`) as
runtime TS, which killed dependency pre-bundling wholesale. The previous
`/* @vite-ignore */` hack kept the scanner away but also meant the
imports failed at runtime, silently dropping Deno type hints in the
TanStack build. Do NOT widen the allowlist to `*.d.ts` — hijacking
declaration-file resolution globally would corrupt packages that ship
`.d.ts` next to their JS. The `as string` casts on the import specifiers
in `AIEditor/index.tsx` keep tsc from resolving the `.d.ts` files as
declaration files (TS2846) while erasing to plain literals both bundlers
statically analyze.
### Other build-side migration changes
- `pnpm-workspace.yaml` catalog now includes `@tanstack/react-router`,
+9 -85
View File
@@ -11,6 +11,8 @@ import {
type Ref,
} from 'react'
import { splitInternalUrl } from '@/lib/internal-url'
// Next's Link accepts either a string `href` or a `UrlObject`
// ({pathname, query, hash}). Workspace source does both — flatten
// `UrlObject` into `pathname?search#hash` first so the TanStack `to`
@@ -62,92 +64,14 @@ function resolveHref(href: string | UrlObject): string {
return `${pathname}${search}${hash}`
}
// Inlined at build time via Vite's `define`. Must agree with Vite `base`
// and `tanstackStart({ router: { basepath } })`. Empty string when no
// basePath is configured. Used to strip a duplicate prefix in
// `splitInternalUrl` below — see the comment there.
const NEXT_PUBLIC_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
// TanStack Link's `to` prop is a route-pattern path; query params and hash
// must be passed separately via `search` / `hash`. Studio code (and Next's
// own contract) routinely passes one of three href shapes:
// 1. a relative path like `/project/abc/editor/123?schema=public`
// 2. a same-origin absolute URL produced by `new URL(...).toString()`,
// e.g. `http://localhost:8082/project/abc/editor/123?schema=public`
// (this is what `buildTableEditorUrl` does)
// 3. a genuinely external URL like `https://supabase.com/docs`.
//
// If we forward any of these straight through to TanStack as `to`, TanStack
// either fails to match a known route pattern (#1 with query) or treats
// the whole thing as external (#2) and falls back to native browser
// navigation — which the user sees as a full page reload.
//
// Split into three parts: pathname, search, hash. Same-origin absolute
// URLs are normalised to a relative path. Cross-origin URLs are left
// alone so TanStack's external-link path handles them.
//
// basePath quirk: TanStack's `to` is **basepath-relative** — given
// `basepath: '/dashboard'` and `to: '/foo'`, TanStack builds the href
// `/dashboard/foo`. Next's contract treats `href` as the **full path
// from app root including basePath**, and studio code routinely
// pre-prefixes BASE_PATH (e.g. `buildTableEditorUrl` calls
// `new URL(`${BASE_PATH}/project/.../editor/...`, location.origin)`).
// Forwarding the BASE_PATH-prefixed pathname as `to` makes TanStack
// double-prefix it (`/dashboard/dashboard/project/...`). Strip the
// basePath when we see it, so what we hand TanStack is always
// basepath-relative.
function splitInternalUrl(url: string): {
to: string
search?: Record<string, string>
hash?: string
} {
// Try to detect cross-origin absolute URLs cheaply before paying for a
// full parse. Protocol-relative URLs (`//host/...`) are always external.
if (url.startsWith('//')) {
return { to: url }
}
// Use the document origin as the parse base so relative inputs resolve.
// SSR has no `location`; fall back to a placeholder host that won't ever
// collide with a real one.
const base =
typeof window !== 'undefined' && window.location ? window.location.origin : 'http://_/'
let parsed: URL
try {
parsed = new URL(url, base)
} catch {
return { to: url }
}
// Cross-origin → leave for TanStack to handle as external.
if (
typeof window !== 'undefined' &&
window.location &&
parsed.origin !== window.location.origin
) {
return { to: url }
}
let pathname = parsed.pathname
// Strip a leading basePath segment so we hand TanStack a basepath-
// relative path. Match `/dashboard` exactly OR `/dashboard/...`; don't
// strip a coincidental prefix like `/dashboard-other`.
if (
NEXT_PUBLIC_BASE_PATH &&
(pathname === NEXT_PUBLIC_BASE_PATH || pathname.startsWith(`${NEXT_PUBLIC_BASE_PATH}/`))
) {
pathname = pathname.slice(NEXT_PUBLIC_BASE_PATH.length) || '/'
}
const search = Object.fromEntries(parsed.searchParams)
const hash = parsed.hash || undefined
return {
to: pathname,
search: Object.keys(search).length > 0 ? search : undefined,
hash,
}
}
// must be passed separately via `search` / `hash`. If we forward an href
// straight through to TanStack as `to`, TanStack either mangles the query
// (path interpolation percent-decodes it and strips control characters) or
// treats a same-origin absolute URL as external and falls back to native
// browser navigation — which the user sees as a full page reload. Splitting
// (plus origin/basePath normalisation) lives in `splitInternalUrl`
// (@/lib/internal-url), shared with the next/router shim.
// Next: prefetch=true|"auto" → eagerly preload; false → never; default
// in production = true. TanStack's preload values are "intent" (on hover/
+29 -8
View File
@@ -3,9 +3,12 @@ import {
useMatches,
useParams as useTanStackParams,
useRouter as useTanStackRouter,
type AnyRouter,
} from '@tanstack/react-router'
import { useMemo } from 'react'
import { splitInternalUrl } from '@/lib/internal-url'
// `next/navigation` is the App Router hook surface (Next 13+). Studio is
// still pages-based, so most of these are entry-points that arrive via
// stray imports or shared packages — we keep the surface comprehensive
@@ -29,13 +32,27 @@ export function useRouter() {
return useMemo(
() => ({
// Split query/hash out of the href — embedding them in TanStack's
// `to` runs the whole string through path interpolation, which
// percent-decodes it and strips control characters from query values
// (see @/lib/internal-url). `search: {}` / `hash: ''` clear stale
// state, matching Next's full-href navigation semantics.
//
// The `<AnyRouter, string>` type arguments opt out of the registered
// route tree's strict typing: Next-style hrefs are free-form strings
// that can't satisfy the route-path union at compile time.
push: (href: string, _options?: NavigateOptions) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
router.navigate({ to: href as any })
const { to, search, hash } = splitInternalUrl(href)
router.navigate<AnyRouter, string>({ to, search: search ?? {}, hash: hash ?? '' })
},
replace: (href: string, _options?: NavigateOptions) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
router.navigate({ to: href as any, replace: true })
const { to, search, hash } = splitInternalUrl(href)
router.navigate<AnyRouter, string>({
to,
search: search ?? {},
hash: hash ?? '',
replace: true,
})
},
// App Router's refresh() refetches the current route's data without
// re-mounting the React tree. Closest TanStack equivalent is
@@ -50,10 +67,14 @@ export function useRouter() {
if (typeof window !== 'undefined') window.history.forward()
},
prefetch: (href: string, _options?: PrefetchOptions) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
router.preloadRoute({ to: href as any }).catch(() => {
// Match Next's fire-and-forget contract.
})
const { to, search, hash } = splitInternalUrl(href)
// `<string, string>` (TFrom, TTo) loosens `to` to a plain string for
// the same free-form-href reason as `navigate` above.
router
.preloadRoute<string, string>({ to, search: search ?? {}, hash: hash ?? '' })
.catch(() => {
// Match Next's fire-and-forget contract.
})
},
}),
[router]
+77 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { resolveUrl } from './router'
import { resolveSearchOrHashOnlyTarget, resolveUrl, withDefaultPathname } from './router'
describe('resolveUrl (next/router compat shim)', () => {
it('returns string URLs untouched', () => {
@@ -58,3 +58,79 @@ describe('resolveUrl (next/router compat shim)', () => {
).toBe('/project/x?a=1')
})
})
describe('resolveSearchOrHashOnlyTarget (next/router compat shim)', () => {
it('prefixes the current pathname on a ?-only target', () => {
// The regression we fixed: `push({ query })` with no pathname produced a
// relative `?...` that TanStack resolved by appending — landing on
// `/advisors/security/?preset=...` (trailing slash injected).
expect(resolveSearchOrHashOnlyTarget('?preset=x', '/advisors/security')).toBe(
'/advisors/security?preset=x'
)
})
it('prefixes the current pathname on a #-only target', () => {
expect(resolveSearchOrHashOnlyTarget('#invoices', '/org/slug/billing')).toBe(
'/org/slug/billing#invoices'
)
})
it('strips a trailing slash from the current pathname, but keeps the root "/"', () => {
expect(resolveSearchOrHashOnlyTarget('?a=1', '/auth/providers/')).toBe('/auth/providers?a=1')
expect(resolveSearchOrHashOnlyTarget('?a=1', '/')).toBe('/?a=1')
expect(resolveSearchOrHashOnlyTarget('?a=1', '')).toBe('/?a=1')
})
it('leaves targets with a pathname untouched', () => {
expect(resolveSearchOrHashOnlyTarget('/project/abc?x=1', '/elsewhere')).toBe('/project/abc?x=1')
expect(resolveSearchOrHashOnlyTarget('/project/abc', '/elsewhere')).toBe('/project/abc')
})
})
describe('withDefaultPathname (next/router compat shim)', () => {
it('fills a missing pathname from the current route pattern so path params are consumed', () => {
// The regression we fixed: `push({ query: { ...router.query, filter } })`
// with no pathname leaked `ref`/`id` into the query string
// (`/editor/17597/?ref=...&id=17597&filter=...`) because nothing
// interpolated them back into the path.
const target = withDefaultPathname(
{ query: { ref: 'abc', id: '17597', filter: 'note:eq:hi' } },
'/project/[ref]/editor/[id]',
{ ref: 'abc', id: '17597' }
)
expect(resolveUrl(target)).toBe('/project/abc/editor/17597?filter=note%3Aeq%3Ahi')
})
it('backfills params the caller omitted from the current route params', () => {
const target = withDefaultPathname(
{ query: { preset: 'WARN' } },
'/project/[ref]/advisors/security',
{ ref: 'abc' }
)
expect(resolveUrl(target)).toBe('/project/abc/advisors/security?preset=WARN')
})
it('caller-provided query values win over backfilled params', () => {
const target = withDefaultPathname({ query: { ref: 'other', page: '2' } }, '/project/[ref]', {
ref: 'abc',
})
expect(resolveUrl(target)).toBe('/project/other?page=2')
})
it('never backfills the TanStack _splat param', () => {
const target = withDefaultPathname({ query: { a: '1' } }, '/org/_/[[...routeSlug]]', {
_splat: 'x/y',
})
expect(resolveUrl(target)).toBe('/org/_/?a=1')
})
it('leaves string URLs, explicit pathnames, and raw-string queries untouched', () => {
expect(withDefaultPathname('/x?a=1', '/p/[ref]', { ref: 'r' })).toBe('/x?a=1')
expect(
withDefaultPathname({ pathname: '/y', query: { a: '1' } }, '/p/[ref]', { ref: 'r' })
).toEqual({ pathname: '/y', query: { a: '1' } })
expect(withDefaultPathname({ query: 'a=1' }, '/p/[ref]', { ref: 'r' })).toEqual({
query: 'a=1',
})
})
})
+80 -7
View File
@@ -4,10 +4,12 @@ import {
useParams,
useSearch,
useRouter as useTanStackRouter,
type AnyRouter,
} from '@tanstack/react-router'
import { useMemo } from 'react'
import { getRouterEventsProxy } from './_router-events'
import { splitInternalUrl } from '@/lib/internal-url'
// Next's pages-router exposes `router.pathname` as the route *pattern*
// (e.g. `/project/[ref]/sql/[id]`), not the resolved URL. TanStack's
@@ -107,6 +109,45 @@ function interpolatePathname(
return { pathname: interpolated, query: rest }
}
// TanStack resolves a `?`- or `#`-only relative `to` by *appending* it to the
// current path, injecting a trailing slash: navigating to `?preset=x` from
// `/advisors/security` lands on `/advisors/security/?preset=x`. Next resolved
// these against the current pathname. Prefix it explicitly (trailing slash
// stripped; root stays `/`) so `push({ query })` with no pathname stays on
// the exact current path.
// Exported for unit tests (see router.test.ts) — not part of the Next surface.
export function resolveSearchOrHashOnlyTarget(to: string, currentPathname: string): string {
if (!to.startsWith('?') && !to.startsWith('#')) return to
let base = currentPathname || '/'
if (base.length > 1 && base.endsWith('/')) base = base.slice(0, -1)
return `${base}${to}`
}
// Next resolves a pathname-less UrlObject against the *current route
// pattern*, re-consuming dynamic params from `query` into the path — e.g.
// `push({ query: { ...router.query, preset } })` on `/project/[ref]/advisors`
// stays on `/project/<ref>/advisors?preset=…`. Because the shim's
// `router.query` merges path params in (Next shape), skipping this would leak
// `ref`/`id` into the query string. Params the caller didn't include are
// backfilled from the current route's params (minus TanStack's `_splat`,
// which no bracket segment can consume) so partial `{ query }` pushes stay
// on-page instead of producing empty path segments.
// Exported for unit tests (see router.test.ts) — not part of the Next surface.
export function withDefaultPathname(
url: string | UrlObject,
currentPathPattern: string,
currentParams: Record<string, QueryValue>
): string | UrlObject {
if (typeof url === 'string' || url.pathname != null) return url
if (!url.query || typeof url.query !== 'object') return url
const { _splat, ...paramsWithoutSplat } = currentParams
return {
...url,
pathname: currentPathPattern,
query: { ...paramsWithoutSplat, ...url.query },
}
}
// Exported for unit tests (see router.test.ts) — not part of the Next surface.
export function resolveUrl(url: string | UrlObject): string {
if (typeof url === 'string') return url
@@ -135,8 +176,8 @@ export function resolveUrl(url: string | UrlObject): string {
// `basepath: '/dashboard'` and `to: '/foo'`, it produces `/dashboard/foo`.
// So we strip the origin AND the basePath when present; otherwise
// `router.push('/dashboard/...')` would double-prefix to
// `/dashboard/dashboard/...`. Mirrors the equivalent logic in the
// next/link shim's `splitInternalUrl`. Cross-origin URLs pass through
// `/dashboard/dashboard/...`. Mirrors the equivalent logic in
// `splitInternalUrl` (@/lib/internal-url). Cross-origin URLs pass through
// untouched so TanStack hands them to the browser as external.
const NEXT_PUBLIC_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
@@ -231,9 +272,33 @@ export function useRouter() {
// It's intentionally not part of Next's public TransitionOptions.
options?: TransitionOptions & { _replace?: boolean }
): Promise<boolean> => {
const to = toRelativeSameOrigin(resolveUrl(url))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await router.navigate({ to: to as any, replace: options?._replace })
// `location.pathname` is already basepath-stripped by TanStack, so the
// prefixed target stays basepath-relative like every other `to`.
const target = resolveSearchOrHashOnlyTarget(
// `useParams({ strict: false })` types as possibly-undefined; treat
// "no params" as an empty record for backfilling.
toRelativeSameOrigin(resolveUrl(withDefaultPathname(url, pathPattern, params ?? {}))),
location.pathname
)
// Never embed `?query`/`#hash` inside TanStack's `to` — router-core
// runs the whole string through path interpolation, which percent-
// decodes it and strips control characters (dropping `%0A` newlines
// from values like the Logs Explorer's `s` SQL param). Split into
// { to, search, hash } instead; the target is already relative and
// basepath-stripped, so splitInternalUrl's own origin/basePath
// normalisation is a no-op here. `search: {}` clears the query,
// matching Next's push-without-query semantics; same for `hash: ''`.
//
// The `<AnyRouter, string>` type arguments opt out of the registered
// route tree's strict typing: Next-style hrefs are free-form strings
// that can't satisfy the route-path union at compile time.
const { to, search, hash } = splitInternalUrl(target)
await router.navigate<AnyRouter, string>({
to,
search: search ?? {},
hash: hash ?? '',
replace: options?._replace,
})
return true
}
@@ -308,8 +373,16 @@ export function useRouter() {
_options?: PrefetchOptions
): Promise<void> => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await router.preloadRoute({ to: url as any })
// Split like navigate() above so a query string in the href
// preloads the real target instead of a path-mangled one.
// `<string, string>` (TFrom, TTo) loosens `to` to a plain string
// for the same free-form-href reason as `navigate` above.
const { to, search, hash } = splitInternalUrl(toRelativeSameOrigin(url))
await router.preloadRoute<string, string>({
to,
search: search ?? {},
hash: hash ?? '',
})
} catch {
// Next's prefetch is fire-and-forget; swallow resolution errors
// (e.g. unknown route) so callers don't have to guard.
+62
View File
@@ -0,0 +1,62 @@
/**
* TanStack-build replacement for `@sentry/nextjs`.
*
* `@sentry/nextjs`'s client entry imports `next/dist/shared/lib/constants`,
* which evaluates `...(process?.features?.typescript ? ['next.config.mts'] : [])`
* at module scope. Optional chaining does NOT guard an undeclared `process`
* identifier in the browser, so any built chunk containing it throws
* `ReferenceError: process is not defined` at load time (observed on the
* table editor route). The Vite dev server shims `process`, so the crash
* only surfaces in the production/test build.
*
* vite.config.ts aliases `@sentry/nextjs` to this module for the TanStack
* build only — app source keeps importing `@sentry/nextjs`, and the Next
* build (`build:next`) is untouched.
*
* `@sentry/nextjs` re-exports `@sentry/react` (same version, 10.x) and adds
* Next-specific helpers on top. Everything Studio uses at runtime —
* `captureException`, `captureMessage`, `startSpan`, `withScope`, `setTag`,
* `setUser`, `init`, and the `Event` / `Breadcrumb` / `StackFrame` types —
* exists in `@sentry/react`, so re-export the lot.
*/
import { captureException } from '@sentry/react'
// eslint-disable-next-line barrel-files/avoid-re-export-all -- compat shim: its whole job is to mirror @sentry/react's surface
export * from '@sentry/react'
// ---------------------------------------------------------------------------
// Next-specific APIs with no `@sentry/react` equivalent. None of them are in
// the TanStack/Vite module graph today (they're only referenced from
// Next-convention files: instrumentation-client.ts, instrumentation.ts and
// next.config.ts), but provide explicit stand-ins so the alias can never
// produce an `undefined is not a function` crash if one leaks in later.
// ---------------------------------------------------------------------------
/**
* `@sentry/nextjs`'s hook for Next's `onRouterTransitionStart` instrumentation
* event. TanStack Router navigation spans would instead come from
* `Sentry.tanstackRouterBrowserTracingIntegration` — no-op here.
*/
export function captureRouterTransitionStart(_href: string, _navigationType: string): void {}
/**
* `@sentry/nextjs`'s hook for Next's `onRequestError` server instrumentation.
* Report the error through the regular capture pipeline instead.
*/
export function captureRequestError(
error: unknown,
_request: unknown,
_errorContext: unknown
): void {
// `captureException` from `@sentry/react` is isomorphic (no-op without an
// initialized client), so this is safe on both server and client.
captureException(error)
}
/**
* Build-time-only wrapper for next.config — meaningless under Vite. Return
* the config unchanged so any accidental usage is inert.
*/
export function withSentryConfig<T>(config: T, ..._options: unknown[]): T {
return config
}
@@ -197,12 +197,14 @@ export function saveTableEditorStateToLocalStorage({
gridColumns,
sorts,
filters,
sensitiveDataColumns,
}: {
projectRef: string
tableId: number
gridColumns?: CalculatedColumn<any, any>[]
sorts?: string[]
filters?: string[]
sensitiveDataColumns?: string[]
}) {
const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
const savedStr = safeSessionStorage.getItem(storageKey) ?? safeLocalStorage.getItem(storageKey)
@@ -211,6 +213,7 @@ export function saveTableEditorStateToLocalStorage({
...(gridColumns !== undefined && { gridColumns }),
...(sorts !== undefined && { sorts: sorts.filter((sort) => sort !== '') }),
...(filters !== undefined && { filters: filters.filter((filter) => filter !== '') }),
...(sensitiveDataColumns !== undefined && { sensitiveDataColumns }),
}
let savedJson
@@ -287,6 +290,7 @@ export const handleCellKeyDown = <TRow extends SupaRow = SupaRow>(
rows: TRow[]
columns: SupaColumn[]
onRowsChange: (rows: TRow[], data: RowsChangeData<TRow, unknown>) => void
sensitiveDataColumns?: Set<string>
}
) => {
const { mode, column, row, rowIdx } = args
@@ -296,11 +300,16 @@ export const handleCellKeyDown = <TRow extends SupaRow = SupaRow>(
if (key === 'c' && (event.metaKey || event.ctrlKey)) {
if (window.getSelection()?.isCollapsed === false) return
const isSensitive = context?.sensitiveDataColumns?.has(column.key as string)
const value = formatClipboardValue(row[column.key] ?? '')
event.preventDefault()
event.preventGridDefault()
void copyToClipboard(value, () => {
toast.success('Copied cell value to clipboard')
if (isSensitive) {
toast.warning('Copied sensitive data to clipboard')
} else {
toast.success('Copied cell value to clipboard')
}
})
return
}
@@ -1,13 +1,23 @@
import { PropsWithChildren } from 'react'
import type { RenderCellProps } from 'react-data-grid'
import { isColumnMasked } from '../../utils/sensitive-data'
import { NullValue } from '../common/NullValue'
import { SupaRow } from '@/components/grid/types'
import { convertByteaToHex } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export const BinaryFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
const snap = useTableEditorTableStateSnapshot()
const value = p.row[p.column.key]
const isMasked = isColumnMasked(
p.column.key as string,
snap.sensitiveDataColumns,
snap.temporarilyRevealedColumns
)
if (!value) return <NullValue />
if (isMasked) return <>••••••••</>
const binaryValue = convertByteaToHex(value)
return <>{binaryValue}</>
}
@@ -2,10 +2,20 @@ import type { PropsWithChildren } from 'react'
import type { RenderCellProps } from 'react-data-grid'
import type { SupaRow } from '../../types'
import { isColumnMasked } from '../../utils/sensitive-data'
import { NullValue } from '../common/NullValue'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export const BooleanFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
const snap = useTableEditorTableStateSnapshot()
const value = p.row[p.column.key] as boolean | null
const isMasked = isColumnMasked(
p.column.key as string,
snap.sensitiveDataColumns,
snap.temporarilyRevealedColumns
)
if (value === null) return <NullValue />
if (isMasked) return <>••••••••</>
return <>{value ? 'TRUE' : 'FALSE'}</>
}
@@ -1,14 +1,26 @@
import { PropsWithChildren } from 'react'
import type { RenderCellProps } from 'react-data-grid'
import { isColumnMasked } from '../../utils/sensitive-data'
import { EmptyValue } from '../common/EmptyValue'
import { NullValue } from '../common/NullValue'
import { SupaRow } from '@/components/grid/types'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export const DefaultFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
const snap = useTableEditorTableStateSnapshot()
let value = p.row[p.column.key]
// Check if column should be masked: marked sensitive AND not temporarily revealed
const isMasked = isColumnMasked(
p.column.key as string,
snap.sensitiveDataColumns,
snap.temporarilyRevealedColumns
)
if (value === null) return <NullValue />
if (value === '') return <EmptyValue />
if (isMasked) return <>••••••••</>
if (typeof value == 'object' || Array.isArray(value)) {
value = JSON.stringify(value)
}
@@ -6,6 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import type { SupaRow } from '../../types'
import { isColumnMasked } from '../../utils/sensitive-data'
import { NullValue } from '../common/NullValue'
import { ReferenceRecordPeek } from './ReferenceRecordPeek'
import { convertByteaToHex } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils'
@@ -14,6 +15,7 @@ import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
import { isTableLike } from '@/data/table-editor/table-editor-types'
import { useTableQuery } from '@/data/tables/table-retrieve-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
interface Props extends PropsWithChildren<RenderCellProps<SupaRow, unknown>> {
tableId?: number
@@ -21,7 +23,13 @@ interface Props extends PropsWithChildren<RenderCellProps<SupaRow, unknown>> {
export const ForeignKeyFormatter = (props: Props) => {
const { tableId, row, column } = props
const snap = useTableEditorTableStateSnapshot()
const { data: project } = useSelectedProjectQuery()
const isMasked = isColumnMasked(
column.key as string,
snap.sensitiveDataColumns,
snap.temporarilyRevealedColumns
)
const { data, isPending: isLoading } = useTableEditorQuery({
projectRef: project?.ref,
@@ -58,7 +66,7 @@ export const ForeignKeyFormatter = (props: Props) => {
return (
<div className="flex w-full items-center justify-between flex justify-between">
<span className="m-0 grow overflow-hidden text-ellipsis">
{formattedValue === null ? <NullValue /> : formattedValue}
{formattedValue === null ? <NullValue /> : isMasked ? '••••••••' : formattedValue}
</span>
{isLoading && formattedValue !== null && (
<div className="w-6 h-6 flex items-center justify-center">
@@ -2,14 +2,23 @@ import { PropsWithChildren } from 'react'
import type { RenderCellProps } from 'react-data-grid'
import type { SupaRow } from '../../types'
import { isColumnMasked } from '../../utils/sensitive-data'
import { EmptyValue } from '../common/EmptyValue'
import { NullValue } from '../common/NullValue'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
export const JsonFormatter = (p: PropsWithChildren<RenderCellProps<SupaRow, unknown>>) => {
const snap = useTableEditorTableStateSnapshot()
let value = p.row[p.column.key]
const isMasked = isColumnMasked(
p.column.key as string,
snap.sensitiveDataColumns,
snap.temporarilyRevealedColumns
)
if (value === null) return <NullValue />
if (value === '') return <EmptyValue />
if (isMasked) return <>••••••••</>
// [Joshen] With reference to table-rows-query, we're only pulling max n characters on text/jsonb columns
// If column value is longer, value will be concatenated with ..., and we just want to make sure the JSON
@@ -429,6 +429,7 @@ export const Grid = memo(
rows: rows ?? [],
columns: snap.table.columns,
onRowsChange,
sensitiveDataColumns: snap.sensitiveDataColumns,
})
}
/>
@@ -1,5 +1,7 @@
import { ArrowDown, ArrowUp, ChevronDown, Copy, Edit, Lock, Trash, Unlock } from 'lucide-react'
import { ArrowDown, ArrowUp, ChevronDown, Copy, Edit, Eye, Lock, Trash, Unlock } from 'lucide-react'
import { useContext, useEffect, useState } from 'react'
import type { CalculatedColumn } from 'react-data-grid'
import { toast } from 'sonner'
import {
Button,
cn,
@@ -17,7 +19,10 @@ import {
import { useTableSort } from '@/components/grid/hooks/useTableSort'
import type { Sort } from '@/components/grid/types'
import { useTableEditorStateSnapshot } from '@/state/table-editor'
import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
import {
TableEditorTableStateContext,
useTableEditorTableStateSnapshot,
} from '@/state/table-editor-table'
interface ColumnMenuProps {
column: CalculatedColumn<any, unknown>
@@ -27,7 +32,11 @@ interface ColumnMenuProps {
export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
const state = useContext(TableEditorTableStateContext)
const { sorts, addOrUpdateSort, removeSort } = useTableSort()
const [tempRevealTimeouts, setTempRevealTimeouts] = useState<Map<string, NodeJS.Timeout>>(
new Map()
)
const columnKey = column.key
const columnName = column.name as string
@@ -54,6 +63,50 @@ export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
}
}
useEffect(() => {
return () => {
tempRevealTimeouts.forEach((timeout) => clearTimeout(timeout))
}
}, [tempRevealTimeouts])
function onToggleSensitiveData() {
const isMasked = snap.sensitiveDataColumns.has(columnKey)
const isTemporarilyRevealed = snap.temporarilyRevealedColumns.has(columnKey)
if (isMasked && !isTemporarilyRevealed) {
// Temporarily reveal for 5 seconds (don't persist)
const existingTimeout = tempRevealTimeouts.get(columnKey)
if (existingTimeout) clearTimeout(existingTimeout)
state.temporarilyRevealedColumns.add(columnKey)
const timeout = setTimeout(() => {
state.temporarilyRevealedColumns.delete(columnKey)
setTempRevealTimeouts((prev) => {
const next = new Map(prev)
next.delete(columnKey)
return next
})
}, 5000)
setTempRevealTimeouts((prev) => new Map(prev).set(columnKey, timeout))
toast.info('Data will be hidden again in 5 seconds')
} else {
// Column is being revealed (either temp or persistent), toggle persistent mask
const existingTimeout = tempRevealTimeouts.get(columnKey)
if (existingTimeout) clearTimeout(existingTimeout)
state.toggleSensitiveDataColumn(columnKey)
state.temporarilyRevealedColumns.delete(columnKey)
setTempRevealTimeouts((prev) => {
const next = new Map(prev)
next.delete(columnKey)
return next
})
}
}
function onSortColumn(ascending: boolean) {
if (!columnKey) return
const currentSort = sorts.find((s) => s.column === columnKey)
@@ -140,6 +193,18 @@ export const ColumnMenu = ({ column, isEncrypted }: ColumnMenuProps) => {
</>
)}
</DropdownMenuItem>
{snap.sensitiveDataColumns.has(columnKey) && (
<DropdownMenuItem
className="space-x-2"
disabled={snap.temporarilyRevealedColumns.has(columnKey)}
onClick={onToggleSensitiveData}
>
<Eye size={14} strokeWidth={1.5} />
<span>
{snap.temporarilyRevealedColumns.has(columnKey) ? 'Data revealed (5s)' : 'Show data'}
</span>
</DropdownMenuItem>
)}
{snap.editable && (
<>
<DropdownMenuSeparator />
@@ -44,17 +44,30 @@ export const RowContextMenuContent = ({
const value = row[column.key]
const text = formatClipboardValue(value)
const isSensitive = snap.sensitiveDataColumns.has(column.key as string)
void copyToClipboard(text, () => {
toast.success('Copied cell value to clipboard')
if (isSensitive) {
toast.warning('Copied sensitive data to clipboard')
} else {
toast.success('Copied cell value to clipboard')
}
})
}, [activeCellPosition, row, snap.gridColumns])
}, [activeCellPosition, row, snap.gridColumns, snap.sensitiveDataColumns])
const onCopyRowContent = useCallback(() => {
const hasSensitiveColumns = snap.gridColumns.some((col) =>
snap.sensitiveDataColumns.has(col.key as string)
)
void copyToClipboard(JSON.stringify(row), () => {
toast.success('Copied row to clipboard')
if (hasSensitiveColumns) {
toast.warning('Copied row containing sensitive data to clipboard')
} else {
toast.success('Copied row to clipboard')
}
})
}, [row])
}, [row, snap.gridColumns, snap.sensitiveDataColumns])
const getRowAndColumn = useCallback(() => {
if (!activeCellPosition) return null
@@ -4,6 +4,7 @@ export interface SavedState {
filters?: string[]
sorts?: string[]
gridColumns: CalculatedColumn<any, any>[]
sensitiveDataColumns?: string[]
}
export interface DragItem {
@@ -0,0 +1,7 @@
export function isColumnMasked(
columnKey: string,
sensitiveDataColumns: Set<string>,
temporarilyRevealedColumns: Set<string>
): boolean {
return sensitiveDataColumns.has(columnKey) && !temporarilyRevealedColumns.has(columnKey)
}
@@ -35,15 +35,15 @@ function useLastSeen({ projectRef, enabled }: { projectRef: string; enabled?: bo
} {
const now = useRef(new Date()).current
const query = useLogsQuery(
const query = useLogsQuery({
enabled,
projectRef,
{
initialParams: {
iso_timestamp_start: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
iso_timestamp_end: now.toISOString(),
sql: "-- last-used-secret-api-keys\nSELECT unix_millis(max(timestamp)) as timestamp, apikey.`hash` FROM edge_logs cross join unnest(metadata) as m cross join unnest(m.request) as request cross join unnest(request.sb) as sb cross join unnest(sb.apikey) as sbapikey cross join unnest(sbapikey.apikey) as apikey WHERE apikey.error is null and apikey.`hash` is not null and apikey.prefix like 'sb_secret_%' GROUP BY apikey.`hash`",
},
enabled
)
})
return useMemo(() => {
if (query.isLoading || !query.logData) {
@@ -18,13 +18,11 @@ export function ApiAuthorizationErrorScreen({
<div className="flex flex-col gap-3 px-6 pb-6">
<Admonition
type="warning"
title="Retry the authorization request from the requesting app."
description={
<>
Retry the authorization request from the requesting app.
{error && (
<span className="mt-1 block text-foreground-lighter">Error: {error.message}</span>
)}
</>
error && (
<span className="mt-1 block text-foreground-lighter">Error: {error.message}</span>
)
}
/>
<Button variant="default" block asChild>
@@ -1,4 +1,5 @@
import dayjs from 'dayjs'
import Link from 'next/link'
import { type ReactNode } from 'react'
import type { UseFormReturn } from 'react-hook-form'
import {
@@ -194,6 +195,12 @@ function OrganizationsEmptyState(): ReactNode {
type="warning"
title="No organizations found"
description="Create an organization before authorizing this request."
actions={[
// [Joshen] JFYI this is a short term solution to guide users with creating an org from here
<Button asChild key="new-org" variant="default">
<Link href="/new">Create an organization</Link>
</Button>,
]}
/>
)
}
@@ -23,7 +23,8 @@ export function ApiAuthorizationInvalidScreen({
<div className="flex flex-col gap-3 px-6 pb-6">
<Admonition
type="warning"
description={`Retry the authorization request from the requesting app. The URL is missing parameter${
title="Retry the authorization request from the requesting app. "
description={`The URL is missing parameter${
isPlural ? 's' : ''
}: ${missingParameters.join(', ')}.`}
/>
@@ -127,6 +127,12 @@ export const useIsJitDbAccessEnabled = () => {
return jitDbAccessEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]
}
export const useIsSqlEditorManualSaveEnabled = () => {
const { flags } = useFeaturePreviewContext()
const sqlEditorManualSaveEnabled = useFlag('sqlEditorManualSave')
return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
}
export const useIsRLSTesterEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]
@@ -35,6 +35,7 @@ import { JitDbAccessPreview } from './JitDbAccessPreview'
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
import { RLSTesterPreview } from './RLSTesterPreview'
import { SqlEditorManualSavePreview } from './SqlEditorManualSavePreview'
import { UnifiedLogsPreview } from './UnifiedLogsPreview'
import { FeaturePreview, useFeaturePreviews } from './useFeaturePreviews'
import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
@@ -52,6 +53,7 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: <PlatformWebhooksPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: <JitDbAccessPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: <RLSTesterPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: <SqlEditorManualSavePreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: <IntegrationsLayoutPreview />,
}
@@ -0,0 +1,17 @@
export const SqlEditorManualSavePreview = () => {
return (
<div className="space-y-4">
<p className="text-sm text-foreground-light">
Switch the SQL Editor from autosaving every edit to saving only when you ask it to.
</p>
<div className="space-y-2">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>Stop auto-saving snippet edits as you type</li>
<li>Add a Save button next to Run in the SQL Editor toolbar</li>
<li>Let you save with Cmd+S at any time</li>
</ul>
</div>
</div>
)
}
@@ -23,6 +23,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
const platformWebhooksEnabled = useFlag('platformWebhooks')
const jitDbAccessEnabled = useFlag('jitDbAccess')
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
const sqlEditorManualSaveEnabled = useFlag('sqlEditorManualSave')
const unifiedLogsDefaultOptIn = useFlag('unifiedLogsDefaultOptIn')
@@ -109,7 +110,22 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
isDefaultOptIn: false,
getRoute: (ref?: string) => `/project/${ref}/integrations`,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE,
name: 'Disable snippet auto-saving',
discussionsUrl: undefined,
isNew: true,
isPlatformOnly: true,
isDefaultOptIn: false,
enabled: sqlEditorManualSaveEnabled,
},
].sort((a, b) => Number(b.isNew) - Number(a.isNew)),
[unifiedLogsDefaultOptIn, platformWebhooksEnabled, jitDbAccessEnabled, isMarketplaceEnabled]
[
unifiedLogsDefaultOptIn,
platformWebhooksEnabled,
jitDbAccessEnabled,
isMarketplaceEnabled,
sqlEditorManualSaveEnabled,
]
)
}
@@ -5,6 +5,7 @@ import { toast } from 'sonner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogBody,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
@@ -14,6 +15,7 @@ import {
AlertDialogTrigger,
Button,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { type AuthTemplate } from './EmailTemplates.types'
import { getAuthTemplateType } from './EmailTemplates.utils'
@@ -32,6 +34,7 @@ export const ResetTemplateDialog = ({
}) => {
const { ref: projectRef } = useParams()
const [open, setOpen] = useState(false)
const [error, setError] = useState<string | null>(null)
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
@@ -40,25 +43,40 @@ export const ResetTemplateDialog = ({
const { id } = template
const templateType = getAuthTemplateType(id)
const { mutate: resetAuthTemplate, isPending: isResetting } = useAuthTemplateResetMutation()
const { mutateAsync: resetAuthTemplate, isPending: isResetting } = useAuthTemplateResetMutation({
onSuccess: (config) => {
toast.success('Email template reset to default')
onResetSuccess(config)
},
onError: () => {},
})
const resetTemplateToDefault = async () => {
if (!projectRef) throw new Error('Project ref is required')
if (!templateType) throw new Error('Template type is required')
resetAuthTemplate(
{ projectRef, template: templateType },
{
onSuccess: (config) => {
toast.success('Email template reset to default')
onResetSuccess(config)
},
}
)
setError(null)
try {
await resetAuthTemplate({ projectRef, template: templateType })
} catch (error) {
setError(
error instanceof Error
? error.message
: 'An unknown error occurred while resetting the template'
)
throw error
}
}
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialog
open={open}
onOpenChange={(next) => {
setOpen(next)
if (!next) setError(null)
}}
>
<AlertDialogTrigger asChild>
<Button variant="default" type="button" disabled={!canUpdateConfig}>
Reset template
@@ -73,15 +91,21 @@ export const ResetTemplateDialog = ({
: 'This will remove your custom subject line and email body content. The default values will be used instead.'}
</AlertDialogDescription>
</AlertDialogHeader>
{error && (
<AlertDialogBody>
<Admonition
type="destructive"
title="Unable to reset email template"
description={error}
/>
</AlertDialogBody>
)}
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isResetting}>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="warning"
loading={isResetting}
onClick={(e) => {
e.preventDefault()
resetTemplateToDefault()
}}
onClick={resetTemplateToDefault}
>
Reset
</AlertDialogAction>
@@ -124,7 +124,10 @@ const renderTemplateEditor = ({
})
useAsyncCheckPermissionsMock.mockReturnValue({ can: canUpdateConfig })
useAuthConfigUpdateMutationMock.mockReturnValue({ mutate: updateAuthConfigMock })
useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock, isPending: false })
useAuthTemplateResetMutationMock.mockImplementation((options = {}) => ({
mutateAsync: (vars: unknown) => resetTemplateMock(vars, options),
isPending: false,
}))
return render(<TemplateEditor template={confirmationTemplate} />)
}
@@ -253,4 +256,22 @@ describe('TemplateEditor reset to default', () => {
expect(screen.getByRole('button', { name: 'Reset template' })).toBeDisabled()
})
it('keeps the dialog open and shows an inline error when reset fails', async () => {
const user = userEvent.setup()
resetTemplateMock.mockImplementation(async () => {
throw new Error('Reset endpoint unavailable')
})
renderTemplateEditor({ hasCustomBody: true })
await user.click(screen.getByRole('button', { name: 'Reset template' }))
const dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'Reset' }))
expect(await within(dialog).findByText('Reset endpoint unavailable')).toBeInTheDocument()
expect(within(dialog).getByText('Unable to reset email template')).toBeInTheDocument()
expect(within(dialog).getByRole('button', { name: 'Reset' })).toBeInTheDocument()
expect(toast.error).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,105 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse } from 'msw'
import { toast } from 'sonner'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SiteUrl from './SiteUrl'
import type { AuthConfigResponse } from '@/data/auth/auth-config-query'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}))
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
useAsyncCheckPermissions: () => ({ can: true, isSuccess: true }),
}))
vi.mock('@/lib/constants', async () => {
const actual = await vi.importActual<Record<string, unknown>>('@/lib/constants')
return {
...actual,
get IS_PLATFORM() {
return true
},
}
})
const CURRENT_SITE_URL = 'https://old.example.com'
const renderSiteUrl = () => customRender(<SiteUrl />)
describe('SiteUrl', () => {
beforeEach(() => {
vi.clearAllMocks()
addAPIMock({
method: 'get',
path: '/platform/auth/:ref/config',
response: { SITE_URL: CURRENT_SITE_URL } as unknown as AuthConfigResponse,
})
})
it('trims leading and trailing whitespace from the site URL before submitting', async () => {
const user = userEvent.setup()
const requests: Array<{ body: unknown }> = []
addAPIMock({
method: 'patch',
path: '/platform/auth/:ref/config',
response: async ({ request }) => {
requests.push({ body: await request.json() })
return HttpResponse.json<AuthConfigResponse>({
SITE_URL: 'https://new.example.com',
} as unknown as AuthConfigResponse)
},
})
renderSiteUrl()
const input = await screen.findByDisplayValue(CURRENT_SITE_URL)
await user.clear(input)
await user.type(input, ' https://new.example.com ')
fireEvent.click(await screen.findByRole('button', { name: 'Save changes' }))
await waitFor(() => expect(requests).toHaveLength(1))
expect(requests[0].body).toEqual({ SITE_URL: 'https://new.example.com' })
expect(toast.success).toHaveBeenCalledWith('Successfully updated site URL')
})
it('shows a validation error and does not submit when the value is only whitespace', async () => {
const user = userEvent.setup()
const requests: Array<{ body: unknown }> = []
addAPIMock({
method: 'patch',
path: '/platform/auth/:ref/config',
response: async ({ request }) => {
requests.push({ body: await request.json() })
return HttpResponse.json<AuthConfigResponse>({
SITE_URL: '',
} as unknown as AuthConfigResponse)
},
})
renderSiteUrl()
const input = await screen.findByDisplayValue(CURRENT_SITE_URL)
await user.clear(input)
await user.type(input, ' ')
fireEvent.click(await screen.findByRole('button', { name: 'Save changes' }))
expect(await screen.findByText('Must have a Site URL')).toBeInTheDocument()
await waitFor(() => expect(requests).toHaveLength(0))
expect(toast.success).not.toHaveBeenCalled()
})
})
@@ -22,7 +22,7 @@ import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-muta
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
const schema = z.object({
SITE_URL: z.string().min(1, 'Must have a Site URL'),
SITE_URL: z.string().trim().min(1, 'Must have a Site URL'),
})
const SiteUrl = () => {
@@ -807,6 +807,7 @@ export const UsersV2 = () => {
renderRow(id, props) {
return (
<Row
key={id}
{...props}
onClick={() => {
const user = users.find((u) => u.id === id)
@@ -1,4 +1,3 @@
import { basename } from 'path'
import { IS_PLATFORM } from 'common'
import { Circle, Code, Minus, Plus, Wind } from 'lucide-react'
import Link from 'next/link'
@@ -7,10 +6,11 @@ import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui'
import { DiffEditor } from '@/components/ui/DiffEditor'
import type { EdgeFunctionBodyData } from '@/data/edge-functions/edge-function-body-query'
import type {
EdgeFunctionsDiffResult,
FileInfo,
FileStatus,
import {
fileKey,
type EdgeFunctionsDiffResult,
type FileInfo,
type FileStatus,
} from '@/hooks/branches/useEdgeFunctionsDiff'
import { EMPTY_ARR } from '@/lib/void'
@@ -31,9 +31,6 @@ interface FunctionDiffProps {
fileInfos: FileInfo[]
}
// Helper to canonicalize file identifiers to prevent mismatch due to differing root paths
const fileKey = (fullPath: string) => basename(fullPath)
// Helper to get the status color for file indicators
const getStatusColor = (status: FileStatus): string => {
switch (status) {
@@ -158,14 +158,17 @@ export const ExtensionRow = ({ extension }: ExtensionRowProps) => {
<Loader2 className="animate-spin" size={16} />
) : (
<Tooltip>
<TooltipTrigger>
<Switch
disabled={disabled}
checked={isOn}
onCheckedChange={() =>
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
}
/>
<TooltipTrigger asChild>
<div>
<Switch
aria-label="Toggle extension"
disabled={disabled}
checked={isOn}
onCheckedChange={() =>
isOn ? setIsDisableModalOpen(true) : setShowConfirmEnableModal(true)
}
/>
</div>
</TooltipTrigger>
{disabled && (
<TooltipContent side="bottom">
@@ -29,7 +29,7 @@ export const PublicationsTableItem = ({
const isProtected = protectedSchemas.map((x) => x.name).includes(table.schema)
const [checked, setChecked] = useState(
selectedPublication.tables?.find((x: any) => x.id == table.id) != undefined
selectedPublication.tables?.find((x) => x.id == table.id) != undefined
)
const { can: canUpdatePublications } = useAsyncCheckPermissions(
@@ -48,14 +48,12 @@ export const PublicationsTableItem = ({
setChecked(!checked)
const publicationTables = publication?.tables ?? []
const exists = publicationTables.some((x: any) => x.id == table.id)
const exists = publicationTables.some((x) => x.id == table.id)
const tables = !exists
? [`${table.schema}.${table.name}`].concat(
publicationTables.map((t: any) => `${t.schema}.${t.name}`)
publicationTables.map((t) => `${t.schema}.${t.name}`)
)
: publicationTables
.filter((x: any) => x.id != table.id)
.map((x: any) => `${x.schema}.${x.name}`)
: publicationTables.filter((x) => x.id != table.id).map((x) => `${x.schema}.${x.name}`)
updatePublications(
{
@@ -93,16 +91,19 @@ export const PublicationsTableItem = ({
</Badge>
) : (
<Tooltip>
<TooltipTrigger>
<Switch
size="small"
disabled={isToggleDisabled}
checked={checked}
onClick={() => {
if (isWarehousePublication) return
toggleReplicationForTable(table, selectedPublication)
}}
/>
<TooltipTrigger asChild>
<div>
<Switch
size="small"
aria-label={`Toggle replication for ${table.name}`}
disabled={isToggleDisabled}
checked={checked}
onClick={() => {
if (isWarehousePublication) return
toggleReplicationForTable(table, selectedPublication)
}}
/>
</div>
</TooltipTrigger>
{isProtected && (
<TooltipContent side="bottom" className="w-64 text-center">
@@ -51,6 +51,7 @@ export const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: initialValues,
})
const { mutate: createDatabaseRole, isPending: isCreating } = useDatabaseRoleCreateMutation({
@@ -207,7 +207,7 @@ export const TableList = ({
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schemas: [selectedSchema],
},
{
select(views) {
@@ -228,7 +228,7 @@ export const TableList = ({
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schemas: [selectedSchema],
},
{
select(materializedViews) {
@@ -251,7 +251,7 @@ export const TableList = ({
{
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: selectedSchema,
schemas: [selectedSchema],
},
{
select(foreignTables) {
@@ -87,16 +87,16 @@ export const EdgeFunctionRecentErrors = ({
logData: recentErrorInvocations,
isLoading: isLoadingRecentErrorInvocations,
error: recentErrorInvocationsError,
} = useLogsQuery(
projectRef as string,
{
} = useLogsQuery({
projectRef: projectRef!,
initialParams: {
sql: recentErrorInvocationsSql,
iso_timestamp_start: isoTimestampStart,
iso_timestamp_end: isoTimestampEnd,
},
isQueryEnabled,
{ useOtel: true }
)
enabled: isQueryEnabled,
options: { useOtel: true },
})
const recentErrorGroupsBase = useMemo(
() => getRecentErrorGroupsBase(recentErrorInvocations),
@@ -106,16 +106,16 @@ export const EdgeFunctionRecentErrors = ({
logData: sinceLastDeployInvocationCountRows,
isLoading: isLoadingSinceLastDeployInvocationCount,
error: sinceLastDeployInvocationCountError,
} = useLogsQuery(
projectRef as string,
{
} = useLogsQuery({
projectRef: projectRef!,
initialParams: {
sql: sinceLastDeployInvocationCountSql,
iso_timestamp_start: isoTimestampStart,
iso_timestamp_end: isoTimestampEnd,
},
Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart),
{ useOtel: true }
)
enabled: Boolean(projectRef && sinceLastDeployInvocationCountSql && isoTimestampStart),
options: { useOtel: true },
})
const relatedExecutionIds = useMemo(
() => getRelatedExecutionIds(recentErrorGroupsBase),
@@ -131,16 +131,16 @@ export const EdgeFunctionRecentErrors = ({
logData: functionRuntimeLogs,
isLoading: isLoadingFunctionRuntimeLogs,
error: functionRuntimeLogsError,
} = useLogsQuery(
projectRef as string,
{
} = useLogsQuery({
projectRef: projectRef!,
initialParams: {
sql: functionRuntimeLogsSql,
iso_timestamp_start: isoTimestampStart,
iso_timestamp_end: isoTimestampEnd,
},
Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart),
{ useOtel: true }
)
enabled: Boolean(projectRef && functionRuntimeLogsSql && isoTimestampStart),
options: { useOtel: true },
})
const queryError =
toAlertError(recentErrorInvocationsError) ?? toAlertError(functionRuntimeLogsError)
@@ -70,6 +70,7 @@ export const ProjectCard = ({
<>
<li className="list-none h-min">
<CardButton
tabIndex={0}
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
className="h-44 px-0! group pt-5 pb-0 overflow-hidden relative"
hideChevron
@@ -77,7 +78,7 @@ export const ProjectCard = ({
<div className="w-full flex flex-col gap-y-4 justify-between px-5">
<div className="flex flex-col gap-y-0.5 relative">
<div className="flex items-center justify-between">
<h5 className="text-sm shrink truncate pr-5">{name}</h5>
<h2 className="text-sm shrink truncate pr-5">{name}</h2>
<div onClick={(e) => e.preventDefault()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -90,6 +91,7 @@ export const ProjectCard = ({
e.preventDefault()
}}
onPointerDown={(e) => e.stopPropagation()}
aria-label={`Project ${name} actions`}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
@@ -12,6 +12,9 @@ import {
DropdownMenuTrigger,
TableCell,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
@@ -81,29 +84,34 @@ export const ProjectTableRow = ({
<TableCell>
<div className="flex flex-col gap-y-2">
<div>
<h5 className="text-sm">{name}</h5>
<button
tabIndex={0}
onClick={handleCopyProjectRef}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleCopyProjectRef(e)
}
}}
className="inline-flex items-center gap-x-1 cursor-pointer border border-transparent border-dashed rounded-sm transition-colors hover:bg-surface-100 hover:border hover:border-strong group font-mono text-xs text-foreground-lighter hover:text-foreground-light px-1 -ml-1"
>
{projectRef}
{isCopied ? (
<Check size={12} strokeWidth={1.25} className="text-brand" />
) : (
<Copy
size={12}
strokeWidth={1.25}
className="opacity-0 group-hover:opacity-100 transition-opacity"
/>
)}
</button>
<h2 className="text-sm">{name}</h2>
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
onClick={handleCopyProjectRef}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleCopyProjectRef(e)
}
}}
className="inline-flex items-center gap-x-1 cursor-pointer border border-transparent border-dashed rounded-sm transition-colors hover:bg-surface-100 hover:border hover:border-strong group font-mono text-xs text-foreground-lighter hover:text-foreground-light px-1 -ml-1"
>
{projectRef}
{isCopied ? (
<Check size={12} strokeWidth={1.25} className="text-brand" />
) : (
<Copy
size={12}
strokeWidth={1.25}
className="opacity-0 group-hover:opacity-100 transition-opacity"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent>Copy project reference</TooltipContent>
</Tooltip>
</div>
{(isGithubIntegrated || isVercelIntegrated || hasPartnerIcon) && (
<div className="flex items-center gap-x-1.5">
@@ -175,7 +183,7 @@ export const ProjectTableRow = ({
<Button
variant="default"
icon={<MoreVertical />}
aria-label="More actions"
aria-label={`Project ${name} actions`}
className="w-7"
onClick={(e) => e.stopPropagation()}
/>
@@ -38,8 +38,9 @@ import {
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import { type CronTableColumn } from './CronJobs.constants'
import { useDatabaseCronJobRunCommandMutation } from '@/data/database-cron-jobs/database-cron-job-run-mutation'
import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
import { type CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
import { useDatabaseCronJobToggleMutation } from '@/data/database-cron-jobs/database-cron-jobs-toggle-mutation'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -75,8 +76,8 @@ const getNextRun = (schedule: string, lastRun?: string) => {
}
interface CronJobTableCellProps {
col: any
row: any
col: CronTableColumn
row: CronJob
onSelectEdit: (job: CronJob) => void
onSelectDelete: (job: CronJob) => void
}
@@ -92,19 +93,20 @@ export const CronJobTableCell = ({
const [showToggleModal, setShowToggleModal] = useState(false)
const value = row?.[col.id]
const value = row?.[col.id as keyof typeof row]
const { jobid, schedule, latest_run, status, active, jobname } = row
const formattedValue =
const formattedValue = (
col.id === 'jobname' && !jobname
? 'No name provided'
: col.id === 'lastest_run'
? !!value
? dayjs(value).valueOf()
? dayjs(value as string).valueOf()
: undefined
: col.id === 'next_run'
? getNextRun(schedule, latest_run)
: value
) as string
const hasValue = col.id === 'next_run' ? !!formattedValue : col.id in row
@@ -148,6 +150,7 @@ export const CronJobTableCell = ({
<DropdownMenuTrigger asChild>
<Button
variant="text"
aria-label="More actions"
loading={isRunning}
className="h-6 w-6"
icon={<MoreVertical />}
@@ -203,13 +206,16 @@ export const CronJobTableCell = ({
if (col.id === 'active') {
return (
<Dialog open={showToggleModal} onOpenChange={setShowToggleModal}>
<DialogTrigger className="flex items-center" onClick={(e) => e.stopPropagation()}>
<Switch
id={`cron-job-active-${jobid}`}
size="medium"
disabled={isToggling}
checked={active}
/>
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
<div className="flex items-center">
<Switch
id={`cron-job-active-${jobid}`}
aria-label={`${active ? 'Disable' : 'Enable'} cron job`}
size="medium"
disabled={isToggling}
checked={active}
/>
</div>
</DialogTrigger>
<DialogContent
onClick={(e) => e.stopPropagation()}
@@ -49,7 +49,7 @@ export type HTTPHeader = { name: string; value: string }
export type HTTPParameter = { name: string; value: string }
type CronTableColumn = {
export type CronTableColumn = {
id: string
name: string
width?: number
@@ -1,4 +1,7 @@
import { useParams } from 'common'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { toast } from 'sonner'
import type { ShowApiKey } from '../../Docs/Docs.types'
import { GeneralContent } from '@/components/interfaces/Docs/GeneralContent'
@@ -16,6 +19,7 @@ interface DocViewProps {
}
export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
const router = useRouter()
const { ref: projectRef, page, resource, rpc } = useParams()
const { data: settings, error: settingsError } = useProjectSettingsV2Query({ projectRef })
@@ -31,6 +35,27 @@ export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
const { resources, rpcs } = buildEntityMaps(paths)
const resourceMissing = !!resource && !(resource in resources)
const rpcMissing = !!rpc && !(rpc in rpcs)
useEffect(() => {
const isSchemaReady = !isLoading && !!jsonSchema && !settingsError && !jsonSchemaError
if (!isSchemaReady || !projectRef) return
if (resourceMissing || rpcMissing) {
toast.error(`${resourceMissing ? 'table/view' : 'function'} could not be found`)
router.replace(`/project/${projectRef}/integrations/data_api/docs`)
}
}, [
isLoading,
jsonSchema,
settingsError,
jsonSchemaError,
projectRef,
resourceMissing,
rpcMissing,
router,
])
if (settingsError || jsonSchemaError) {
return <DocViewError error={settingsError || jsonSchemaError} />
}
Loaded 100 of 374 files, more files were not shown because too many files have changed in this diff. Show more