Merge branch 'master' into chore/refactor-edit-vault-secret-modal

This commit is contained in:
Drake Costa committed 2025-07-17 08:49:10 -07:00
commit 3273d48e39
450 files changed
+48048 -18723

No files matched your search

+3 -1
View File
@@ -27,4 +27,6 @@ apps/cms/config/api.ts
# files auto-generated by payload cms
apps/cms/src/app/*
apps/cms/src/migrations/*
apps/cms/src/payload-types.ts
apps/cms/src/payload-types.ts
# ignore because of <br/><br/>
apps/www/_blog/2025-07-14-supabase-ui-platform-kit.mdx
+1 -1
View File
@@ -35,7 +35,7 @@ To ensure a positive and inclusive environment, please read our [code of conduct
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
- [Git](https://git-scm.com/)
- [Node.js v20.x (LTS)](https://nodejs.org)
- [Node.js v22.x or higher](https://nodejs.org)
- [pnpm](https://pnpm.io/) version 9.x.x or higher
- [make](https://www.gnu.org/software/make/) or the equivalent to `build-essentials` for your OS
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
@@ -13,7 +13,7 @@ vi.mock(import('~/lib/openAi'), () => ({
}))
const rpcSpy = vi.fn().mockImplementation((funcName, params) => {
if (funcName === 'search_content') {
if (funcName === 'search_content_hybrid') {
const limit = params?.max_result || 2
const mockResults = [
{
@@ -88,7 +88,7 @@ describe('/api/graphql searchDocs', () => {
expect(json.data).toBeDefined()
expect(json.data.searchDocs).toBeDefined()
expect(json.data.searchDocs.nodes).toBeInstanceOf(Array)
expect(json.data.searchDocs.nodes).toHaveLength(2)
expect(json.data.searchDocs.nodes).toHaveLength(3)
expect(json.data.searchDocs.nodes[0]).toMatchObject({
title: 'Test Guide',
href: '/guides/test',
@@ -117,7 +117,7 @@ describe('/api/graphql searchDocs', () => {
expect(json.data.searchDocs.nodes).toHaveLength(1)
expect(json.data.searchDocs.nodes[0].title).toBe('Test Guide')
expect(rpcSpy).toHaveBeenCalledWith(
'search_content',
'search_content_hybrid',
expect.objectContaining({
max_result: 1,
})
@@ -146,7 +146,7 @@ describe('/api/graphql searchDocs', () => {
expect(json.errors).toBeUndefined()
expect(json.data.searchDocs.nodes[0].content).toBe('Test content')
expect(rpcSpy).toHaveBeenCalledWith(
'search_content',
'search_content_hybrid',
expect.objectContaining({
include_full_content: true,
})
@@ -718,8 +718,12 @@ export const auth = {
{ name: 'Password Security', url: '/guides/auth/password-security' },
{ name: 'Rate Limits', url: '/guides/auth/rate-limits' },
{ name: 'Bot Detection (CAPTCHA)', url: '/guides/auth/auth-captcha' },
{ name: 'JWTs', url: '/guides/auth/jwts' },
{ name: 'JWT Fields Reference', url: '/guides/auth/jwt-fields' },
{
name: 'JSON Web Tokens (JWT)',
url: '/guides/auth/jwts',
items: [{ name: 'Claims Reference', url: '/guides/auth/jwt-fields' }],
},
{ name: 'JWT Signing Keys', url: '/guides/auth/signing-keys' },
{ name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
{
name: 'Column Level Security',
@@ -1713,6 +1717,25 @@ export const storage: NavMenuConstant = {
{ name: 'API Compatibility', url: '/guides/storage/s3/compatibility' },
],
},
{
name: 'Analytics Buckets',
url: undefined,
items: [
{ name: 'Introduction', url: '/guides/storage/analytics/introduction' },
{
name: 'Creating Analytics Buckets',
url: '/guides/storage/analytics/creating-analytics-buckets',
},
{
name: 'Connecting to Analytics Buckets',
url: '/guides/storage/analytics/connecting-to-analytics-bucket',
},
{
name: 'Limits',
url: '/guides/storage/analytics/limits',
},
],
},
{
name: 'CDN',
url: undefined,
@@ -2065,6 +2088,7 @@ export const security: NavMenuConstant = {
name: 'Product security',
url: undefined,
items: [
{ name: 'Platform configuration', url: '/guides/security/platform-security' },
{ name: 'Product configuration', url: '/guides/security/product-security' },
{ name: 'Security testing', url: '/guides/security/security-testing' },
],
@@ -2147,12 +2171,8 @@ export const platform: NavMenuConstant = {
url: '/guides/platform/multi-factor-authentication',
items: [
{
name: 'Enable MFA',
url: '/guides/platform/multi-factor-authentication',
},
{
name: 'Require MFA for organization members',
url: '/guides/platform/org-mfa-enforcement',
name: 'Enforce MFA on organization',
url: '/guides/platform/mfa/org-mfa-enforcement',
},
],
},
@@ -2337,6 +2357,10 @@ export const telemetry: NavMenuConstant = {
name: 'Log drains',
url: '/guides/telemetry/log-drains',
},
{
name: 'Reports',
url: '/guides/telemetry/reports',
},
{
name: 'Metrics',
url: '/guides/telemetry/metrics',
@@ -2430,7 +2454,26 @@ export const deployment: NavMenuConstant = {
items: [
{ name: 'Managing environments', url: '/guides/deployment/managing-environments' },
{ name: 'Database migrations', url: '/guides/deployment/database-migrations' },
{ name: 'Branching', url: '/guides/deployment/branching' },
],
},
{
name: 'Branching',
url: undefined,
items: [
{ name: 'Overview', url: '/guides/deployment/branching' },
{ name: 'GitHub integration', url: '/guides/deployment/branching/github-integration' },
{
name: 'Branching 2.0 (Alpha)',
url: '/guides/deployment/branching/branching-2',
},
{
name: 'Working with branches',
url: '/guides/deployment/branching/working-with-branches',
},
{ name: 'Configuration', url: '/guides/deployment/branching/configuration' },
{ name: 'Integrations', url: '/guides/deployment/branching/integrations' },
{ name: 'Troubleshooting', url: '/guides/deployment/branching/troubleshooting' },
{ name: 'Billing', url: '/guides/platform/manage-your-usage/branching' },
],
},
{
@@ -585,7 +585,7 @@ const subjects = {
signup: 'Confirm Your Email',
recovery: 'Reset Your Password',
invite: 'You have been invited',
magic_link: 'Your Magic Link',
magiclink: 'Your Magic Link',
email_change: 'Confirm Email Change',
email_change_new: 'Confirm New Email Address',
reauthentication: 'Confirm Reauthentication',
@@ -594,7 +594,7 @@ const subjects = {
signup: 'Confirma tu correo electrónico',
recovery: 'Restablece tu contraseña',
invite: 'Has sido invitado',
magic_link: 'Tu enlace mágico',
magiclink: 'Tu enlace mágico',
email_change: 'Confirma el cambio de correo electrónico',
email_change_new: 'Confirma la Nueva Dirección de Correo',
reauthentication: 'Confirma la reautenticación',
@@ -603,7 +603,7 @@ const subjects = {
signup: 'Confirmez votre adresse e-mail',
recovery: 'Réinitialisez votre mot de passe',
invite: 'Vous avez été invité',
magic_link: 'Votre Lien Magique',
magiclink: 'Votre Lien Magique',
email_change: 'Confirmez le changement d’adresse e-mail',
email_change_new: 'Confirmez la nouvelle adresse e-mail',
reauthentication: 'Confirmez la réauthentification',
@@ -616,7 +616,7 @@ const templates = {
signup: `<h2>Confirm your email</h2><p>Follow this link to confirm your email:</p><p><a href="{{confirmation_url}}">Confirm your email address</a></p><p>Alternatively, enter the code: {{token}}</p>`,
recovery: `<h2>Reset password</h2><p>Follow this link to reset the password for your user:</p><p><a href="{{confirmation_url}}">Reset password</a></p><p>Alternatively, enter the code: {{token}}</p>`,
invite: `<h2>You have been invited</h2><p>You have been invited to create a user on {{site_url}}. Follow this link to accept the invite:</p><p><a href="{{confirmation_url}}">Accept the invite</a></p><p>Alternatively, enter the code: {{token}}</p>`,
magic_link: `<h2>Magic Link</h2><p>Follow this link to login:</p><p><a href="{{confirmation_url}}">Log In</a></p><p>Alternatively, enter the code: {{token}}</p>`,
magiclink: `<h2>Magic Link</h2><p>Follow this link to login:</p><p><a href="{{confirmation_url}}">Log In</a></p><p>Alternatively, enter the code: {{token}}</p>`,
email_change: `<h2>Confirm email address change</h2><p>Follow this link to confirm the update of your email address from {{old_email}} to {{new_email}}:</p><p><a href="{{confirmation_url}}">Change email address</a></p><p>Alternatively, enter the codes: {{token}} and {{new_token}}</p>`,
email_change_new: `<h2>Confirm New Email Address</h2><p>Follow this link to confirm your new email address:</p><p><a href="{{confirmation_url}}">Confirm new email address</a></p><p>Alternatively, enter the code: {{new_token}}</p>`,
reauthentication: `<h2>Confirm reauthentication</h2><p>Enter the code: {{token}}</p>`,
@@ -625,7 +625,7 @@ const templates = {
signup: `<h2>Confirma tu correo electrónico</h2><p>Sigue este enlace para confirmar tu correo electrónico:</p><p><a href="{{confirmation_url}}">Confirma tu correo electrónico</a></p><p>Alternativamente, ingresa el código: {{token}}</p>`,
recovery: `<h2>Restablece tu contraseña</h2><p>Sigue este enlace para restablecer la contraseña de tu usuario:</p><p><a href="{{confirmation_url}}">Restablece tu contraseña</a></p><p>Alternativamente, ingresa el código: {{token}}</p>`,
invite: `<h2>Has sido invitado</h2><p>Has sido invitado para crear un usuario en {{site_url}}. Sigue este enlace para aceptar la invitación:</p><p><a href="{{confirmation_url}}">Aceptar la invitación</a></p><p>Alternativamente, ingresa el código: {{token}}</p>`,
magic_link: `<h2>Tu enlace mágico</h2><p>Sigue este enlace para iniciar sesión:</p><p><a href="{{confirmation_url}}">Iniciar sesión</a></p><p>Alternativamente, ingresa el código: {{token}}</p>`,
magiclink: `<h2>Tu enlace mágico</h2><p>Sigue este enlace para iniciar sesión:</p><p><a href="{{confirmation_url}}">Iniciar sesión</a></p><p>Alternativamente, ingresa el código: {{token}}</p>`,
email_change: `<h2>Confirma el cambio de correo electrónico</h2><p>Sigue este enlace para confirmar la actualización de tu correo electrónico de {{old_email}} a {{new_email}}:</p><p><a href="{{confirmation_url}}">Cambiar correo electrónico</a></p><p>Alternativamente, ingresa los códigos: {{token}} y {{new_token}}</p>`,
email_change_new: `<h2>Confirma la Nueva Dirección de Correo</h2><p>Sigue este enlace para confirmar tu nueva dirección de correo electrónico:</p><p><a href="{{confirmation_url}}">Confirma la nueva dirección de correo</a></p><p>Alternativamente, ingresa el código: {{new_token}}</p>`,
reauthentication: `<h2>Confirma la reautenticación</h2><p>Ingresa el código: {{token}}</p>`,
@@ -634,7 +634,7 @@ const templates = {
signup: `<h2>Confirmez votre adresse e-mail</h2><p>Suivez ce lien pour confirmer votre adresse e-mail :</p><p><a href="{{confirmation_url}}">Confirmez votre adresse e-mail</a></p><p>Vous pouvez aussi saisir le code : {{token}}</p>`,
recovery: `<h2>Réinitialisez votre mot de passe</h2><p>Suivez ce lien pour réinitialiser votre mot de passe :</p><p><a href="{{confirmation_url}}">Réinitialisez votre mot de passe</a></p><p>Vous pouvez aussi saisir le code : {{token}}</p>`,
invite: `<h2>Vous avez été invité</h2><p>Vous avez été invité à créer un utilisateur sur {{site_url}}. Suivez ce lien pour accepter l'invitation :</p><p><a href="{{confirmation_url}}">Acceptez l'invitation</a></p><p>Vous pouvez aussi saisir le code : {{token}}</p>`,
magic_link: `<h2>Votre Lien Magique</h2><p>Suivez ce lien pour vous connecter :</p><p><a href="{{confirmation_url}}">Connectez-vous</a></p><p>Vous pouvez aussi saisir le code : {{token}}</p>`,
magiclink: `<h2>Votre Lien Magique</h2><p>Suivez ce lien pour vous connecter :</p><p><a href="{{confirmation_url}}">Connectez-vous</a></p><p>Vous pouvez aussi saisir le code : {{token}}</p>`,
email_change: `<h2>Confirmez le changement d’adresse e-mail</h2><p>Suivez ce lien pour confirmer la mise à jour de votre adresse e-mail de {{old_email}} à {{new_email}} :</p><p><a href="{{confirmation_url}}">Changez d’adresse e-mail</a></p><p>Vous pouvez aussi saisir les codes : {{token}} et {{new_token}}</p>`,
email_change_new: `<h2>Confirmez la nouvelle adresse e-mail</h2><p>Suivez ce lien pour confirmer votre nouvelle adresse e-mail :</p><p><a href="{{confirmation_url}}">Confirmez la nouvelle adresse e-mail</a></p><p>Vous pouvez aussi saisir le code : {{new_token}}</p>`,
reauthentication: `<h2>Confirmez la réauthentification</h2><p>Saisissez le code : {{token}}</p>`,
@@ -736,7 +736,7 @@ const subjects = {
signup: 'Confirm Your Email',
recovery: 'Reset Your Password',
invite: 'You have been invited',
magic_link: 'Your Magic Link',
magiclink: 'Your Magic Link',
email_change: 'Confirm Email Change',
email_change_new: 'Confirm New Email Address',
reauthentication: 'Confirm Reauthentication'
@@ -747,7 +747,7 @@ const templates = {
signup: `<h2>Confirm your email</h2><p>Follow this link to confirm your email:</p><p><a href="{{confirmation_url}}">Confirm your email address</a></p><p>Alternatively, enter the code: {{token}}</p>`,
recovery: `<h2>Reset password</h2><p>Follow this link to reset the password for your user:</p><p><a href="{{confirmation_url}}">Reset password</a></p><p>Alternatively, enter the code: {{token}}</p>`,
invite: `<h2>You have been invited</h2><p>You have been invited to create a user on {{site_url}}. Follow this link to accept the invite:</p><p><a href="{{confirmation_url}}">Accept the invite</a></p><p>Alternatively, enter the code: {{token}}</p>`,
magic_link: `<h2>Magic Link</h2><p>Follow this link to login:</p><p><a href="{{confirmation_url}}">Log In</a></p><p>Alternatively, enter the code: {{token}}</p>`,
magiclink: `<h2>Magic Link</h2><p>Follow this link to login:</p><p><a href="{{confirmation_url}}">Log In</a></p><p>Alternatively, enter the code: {{token}}</p>`,
email_change: `<h2>Confirm email address change</h2><p>Follow this link to confirm the update of your email address from {{old_email}} to {{new_email}}:</p><p><a href="{{confirmation_url}}">Change email address</a></p><p>Alternatively, enter the codes: {{token}} and {{new_token}}</p>`,
email_change_new: `<h2>Confirm New Email Address</h2><p>Follow this link to confirm your new email address:</p><p><a href="{{confirmation_url}}">Confirm new email address</a></p><p>Alternatively, enter the code: {{new_token}}</p>`,
reauthentication: `<h2>Confirm reauthentication</h2><p>Enter the code: {{token}}</p>`
+5 -5
View File
@@ -1,10 +1,10 @@
---
id: 'jwt-fields'
title: 'JWT Fields Reference'
subtitle: 'Complete reference for JWT fields in Supabase'
title: 'JWT Claims Reference'
subtitle: 'Complete reference for claims appearing in JWTs created by Supabase Auth'
---
This page provides a comprehensive reference for all JWT fields used in Supabase authentication tokens. This information is essential for server-side JWT validation and serialization, especially when implementing authentication in languages like Rust where field names like `ref` are reserved keywords.
This page provides a comprehensive reference for all JWT claims used in Supabase authentication tokens. This information is essential for server-side JWT validation and serialization, especially when implementing authentication in languages like Rust where field names like `ref` are reserved keywords.
## JWT structure overview
@@ -46,7 +46,7 @@ These claims may be present depending on the authentication context:
| `user_metadata` | `object` | **User Metadata** - User-specific data | `{"name": "John Doe"}` |
| `amr` | `array` | **Authentication Methods Reference** - List of authentication methods used | `[{"method": "password", "timestamp": 1640991600}]` |
## Special fields
## Special claims
| Field | Type | Description | Example | Context |
| ----- | -------- | --------------------------------------------------- | ------------------------ | ----------------------------- |
@@ -167,7 +167,7 @@ struct JwtClaims {
role: String,
iat: i64,
exp: i64,
// ... other fields
// ... other claims
}
```
+191 -126
View File
@@ -1,180 +1,245 @@
---
id: 'auth-jwts'
title: 'JWTs'
subtitle: 'JSON Web Tokens'
tocVideo: 'v3Exg5YpJvE'
title: 'JSON Web Token (JWT)'
subtitle: 'Information on how best to use JSON Web Tokens with Supabase'
---
A [JSON Web Token](https://jwt.io/introduction) is a type of data structure, represented as a string, that usually contains identity and authorization information about a user. It encodes information about its lifetime and is signed with a cryptographic key to make it tamper-resistant.
Supabase Access Tokens are JWTs. The JWT is sent along with every request to Supabase services. By verifying the token and inspecting the included claims, you can allow or deny access to resources. [Row Level Security](/docs/guides/database/postgres/row-level-security) policies are based on the information present in JWTs.
Supabase Auth continuously issues a new JWT for each user session, for as long as the user remains signed in. Check the comprehensive guide on [Sessions](/docs/guides/sessions) to find out how you can tailor this process for your needs.
## Encoding and signing JWTs
JWTs provide the foundation for [Row Level Security](/docs/guides/database/row-level-security). Each Supabase product is able to securely decode and verify the validity of a JWT it receives before using Postgres policies and roles to authorize access to the project's data.
JWTs are encoded and signed as follows.
Supabase provides a comprehensive system of managing [JWT Signing Keys](/docs/guides/auth/signing-keys) used to create and verify JSON Web Tokens.
The JSON object starts out looking something like this:
## Introduction
```js
JWTs are strings that have the following structure:
```
<header>.<payload>.<signature>
```
Each part is a string of [Base64-URL](https://en.wikipedia.org/wiki/Base64#Variants_summary_table) encoded JSON, or bytes for the signature.
**Header**
```json
{
"sub": "0001",
"name": "Sam Vimes",
"iat": 1516239022,
"exp": 1518239022
"typ": "JWT",
"alg": "<HS256 | ES256 | RS256>",
"kid": "<unique key identifier>"
}
```
`sub` is the "subject", which is usually the UUID of the user. `name` is self-explanatory, and `iat` is the Unix timestamp at which the token was created. Many JWTs will also have an `exp`, which is the date at which the token is set to expire and can no longer be used. These are some of the standard fields you may find in a JWT, but you can pretty much store whatever you want in there, for example:
Gives some basic identifying information about the string, indicating its type `typ`, the cryptographic algorithm `alg` that can be used to verify the data, and optionally the unique key identifier that should be used when verifying it.
```js
**Payload**
```json
{
"sub": "0002",
"name": "Věra Hrabánková",
"iat": 1516239022,
"exp": 1518239022,
"theme": {
"primary" : "#D80C14",
"secondary" : "#FFFFFF"
}
"iss": "https://project_id.supabase.co/auth/v1",
"exp": 12345678,
"sub": "<user ID>",
"role": "authenticated",
"email": "someone@example.com",
"phone": "+15552368"
// ...
}
```
Just note that the more data you store in your token, the longer the encoded string will be.
Provides identifying information (called "claims") about the user (or other entity) that is represented by the token. Usually a JWT conveys information about what the user can access (then called Access Token) or who the user is (then called ID Token). You can use a [Custom Access Token Hook](/docs/guides/auth/auth-hooks/custom-access-token-hook) to add, remove or change claims present in the token. A few claims are important:
When we want to send the JWT to the user, we first encode the data using an algorithm such as `HS256`. There are many libraries (and several different algorithms) that can be used to do this encoding/decoding, such as [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken). The signing is as simple as:
| Claim | Description |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss` | Identifies the server which issued the token. If you append `/.well-known/jwks.json` to this URL you'll get access to the public keys with which you can verify the token. |
| `exp` | Sets a time limit after which the token should not be trusted and is considered expired, even if it is properly signed. |
| `sub` | Means _subject_, is the unique ID of the user represented by the token. |
| <span className="whitespace-nowrap!">`role`</span> | The Postgres role to use when applying Row Level Security policies. |
| ... | All other claims are useful for quick access to profile information without having to query the database or send a request to the Auth server. |
```js
// from https://replit.com/@awalias/jsonwebtokens#index.js
let token = jwt.sign({ name: 'Sam Vimes' }, 'some-secret')
```
**Signature**
And the resulting string will look like this:
A [digital signature](https://en.wikipedia.org/wiki/Digital_signature) using a [shared secret](https://en.wikipedia.org/wiki/HMAC) or [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography). The purpose of the signature is to verify the authenticity of the `<header>.<payload>` string without relying on database access, liveness or performance of the Auth server. To verify the signature avoid implementing the algorithms yourself and instead rely on `supabase.auth.getClaims()`, or other high-quality JWT verification libraries for your language.
```js
eyJhbGciOiJIUzI1NiJ9
.eyJzdWIiOiIwMDAxIiwibmFtZSI6IlNhbSBWaW1lcyIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE4MjM5MDIyfQ
.zMcHjKlkGhuVsiPIkyAkB2rjXzyzJsMMgpvEGvGtjvA
```
## Supabase and JWTs
You will notice that the string is actually made up of three components:
Supabase creates JWTs in these cases for you:
The first segment `eyJhbGciOiJIUzI1NiJ9` is known as the "header", and when decoded just tells us which algorithm was used to do the encoding:
1. When using Supabase Auth, an access token (JWT) is created for each user while they remain signed in. These are short lived, so they are continuously issued as your user interacts with Supabase APIs.
2. As the legacy JWT-based [API keys](/docs/guides/api/api-keys) `anon` and `service_role`. These have a 10 year expiry and are signed with a shared secret, making them hard to rotate or expire. These JWTs express public access via the `anon` key, or elevated access via the `service_role` key. We strongly recommend switching to publishable and secret API keys.
3. On-the-fly when using publishable or secret API keys. Each API key is transformed into a short-lived JWT that is then used to authorize access to your data. Accessing these short-lived tokens is generally not possible.
```js
{
"alg": "HS256"
}
```
In addition to creating JWTs, Supabase can also accept JWTs from other Auth servers via the [Third-Party Auth](/docs/guides/auth/third-party/overview) feature or ones you've made yourself using the legacy JWT secret or if you've imported in [JWT Signing Key](/docs/guides/auth/signing-keys).
The second segment <code className="break-all">eyJzdWIiOiIwMDAxIiwibmFtZSI6IlNhbSBWaW1lcyIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE4MjM5MDIyfQ</code> contains our original payload:
## Using custom or third-party JWTs
```js
{
"sub": "0001",
"name": "Sam Vimes",
"iat": 1516239022,
"exp": 1518239022
}
```
<Admonition type="note">
The last segment `zMcHjKlkGhuVsiPIkyAkB2rjXzyzJsMMgpvEGvGtjvA` is the signature itself, which is the part used by the website or service provider to verify that a token sent by some user is legitimate. It is produced in the first instance by running the cryptographic function HS256 on the following input:
The `supabase.auth.getClaims()` method is meant to be used only with JWTs issued by Supabase Auth. If you make your own JWTs using the legacy JWT secret or a key you've imported, the verification may fail. We strongly recommend using a JWT verification library for your language to verify this type of JWT based on the claims you're adding in them.
```js
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload)
<jwt_secret>
)
```
</Admonition>
You can test out minting your own tokens on [https://jwt.io](https://jwt.io).
Your Supabase project accepts a JWT in the `Authorization: Bearer <jwt>` header. If you're using the Supabase client library, it does this for you.
It is important to note that anyone who possesses the `jwt_secret` here can create new tokens, and also verify existing ones. More advanced JWT algorithms use two secrets: one for the creation of tokens, and a separate one to verify the validity of signed tokens.
If you are already using Supabase Auth, when a user is signed in, their access token JWT is automatically managed and sent for you with every API call.
You might wonder why JWTs are so popular all of a sudden. The answer is that with the mass adoption of microservice architecture, we were in a situation where several distinct microservices (APIs, websites, servers, etc.) want to validate that a user is who they say they are, or are in other words a "logged-in" user. Traditional session tokens are no use here, since they would require each microservice to either maintain a record of currently valid session tokens or to query a central database each time a user wants to access a resource in order to check the validity of the session token – very inefficient indeed. JWT-based auth in this sense is decentralized, since anyone with the `jwt_secret` can verify a token without needing access to a centralized database.
If you wish to send a JWT from a Third-Party Auth provider, or one you made yourself by using the legacy JWT secret or a JWT signing key you imported, you can pass it to the client library using the `accessToken` option.
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
<Tabs type="underlined" queryGroup="language">
Note: One downside of JWTs is that they are not easily voidable, unlike session tokens. If a JWT is leaked to a malicious actor, they will be able to redeem it anywhere until the expiry date is reached – unless of course the system owner updates the `jwt_secret` (which will of course invalidate _everyone's_ existing tokens).
<TabPanel id="ts" label="TypeScript">
## JWTs in Supabase
```typescript
import { createClient } from '@supabase/supabase-js'
In Supabase we issue JWTs for three different purposes:
1. `anon key`: This key is used to bypass the Supabase API gateway and can be used in your client-side code.
2. `service role key`: This key has super admin rights and can bypass your Row Level Security. Do not put it in your client-side code. Keep it private.
3. `user specific jwts`: These are tokens we issue to users who log into your project/service/website. It's the modern equivalent of a session token, and can be used by a user to access content or permissions specific to them.
The first token here, the `anon key` token, is for developers to send along with their API requests whenever they want to interact with their Supabase database.
Let's say you want to read the names of all the rows in a table `colors`. We would make a request like:
```bash
curl 'https://xscduanzzfseqszwzhcy.supabase.co/rest/v1/colors?select=name' \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxNDIwNTE3NCwiZXhwIjoxOTI5NzgxMTc0fQ.-NBR1WnZyQGpRLdXJfgfpszoZ0EeE6KHatJsDPLIX8c"
```
If we put this token into https://jwt.io, we see it decodes to:
```js
{
"role": "anon",
"iss": "supabase",
"iat": 1614205174,
"exp": 1929781174
}
```
This JWT is signed by a `jwt_secret` specific to the developer's Supabase token (you can find this secret alongside this encoded "anon key" on your Dashboard under Settings > API page) and is required to get past the Supabase API gateway and access the developer's project.
The idea with this particular key is that it's safe to put into your client, meaning it's okay if your end users see this key – but _only_ if you first enable Row Level Security.
The second key, `service role key`, should only ever be used on one of your own servers or environments, and should never be shared with end users. You might use this token to do things like make batch inserts of data.
The `user access token` is the JWT issued when you call for example:
```js
supabase.auth.signIn({
email: 'valid.email@supabase.io',
password: 'They_Live_1988!',
const supabase = createClient('https://<supabase-project>.supabase.co', 'SUPABASE_ANON_KEY', {
accessToken: async () => {
return '<your JWT here>'
},
})
```
This token should be passed in addition to the `apikey` header as an `Authorization Bearer` header like:
</TabPanel>
```bash
curl 'https://xscduanzzfseqszwzhcy.supabase.co/rest/v1/colors?select=name' \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxNDIwNTE3NCwiZXhwIjoxOTI5NzgxMTc0fQ.-NBR1WnZyQGpRLdXJfgfpszoZ0EeE6KHatJsDPLIX8c" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjE1ODI0Mzg4LCJzdWIiOiIwMzM0NzQ0YS1mMmEyLTRhYmEtOGM4YS02ZTc0OGY2MmExNzIiLCJlbWFpbCI6InNvbWVvbmVAZW1haWwuY29tIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwifSwidXNlcl9tZXRhZGF0YSI6bnVsbCwicm9sZSI6ImF1dGhlbnRpY2F0ZWQifQ.I-_oSsJamtinGxniPETBf-ezAUwDW2sY9bJIThvdX9s"
<TabPanel id="dart" label="Flutter">
```dart
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
debug: false,
accessToken: () async {
return "<your JWT here>";
},
);
```
You'll notice that this token is quite a bit longer, since it contains information specific to the user such as:
</TabPanel>
```js
{
"aud": "authenticated",
"exp": 1615824388,
"sub": "0334744a-f2a2-4aba-8c8a-6e748f62a172",
"email": "valid.email@supabase.io",
"app_metadata": {
"provider": "email"
},
"user_metadata": null,
"role": "authenticated"
<TabPanel id="swift" label="Swift (iOS)">
```swift
import Supabase
let supabase = SupabaseClient(
supabaseURL: URL(string: "https://<supabase-project>.supabase.co")!,
supabaseKey: "SUPABASE_ANON_KEY",
options: SupabaseClientOptions(
auth: SupabaseClientOptions.AuthOptions(
accessToken: {
return "<your JWT here>"
}
)
)
)
```
</TabPanel>
<TabPanel id="kotlin" label="Kotlin">
```kotlin
val supabase = createSupabaseClient(
"https://<supabase-project>.supabase.co",
"SUPABASE_ANON_KEY"
) {
accessToken = {
"<your JWT here>"
}
}
```
If using the service role key, you'll need to pass it into both the `apikey` and `authorization` headers (again, only do this from a secure environment such as your own server):
</TabPanel>
```bash
curl "$YOUR_PROJECT_URL/rest/v1/colors?select=name" \
-H "apikey: $YOUR_SERVICE_ROLE_KEY" \
-H "authorization: Bearer $YOUR_SERVICE_ROLE_KEY"
</Tabs>
In the past there was a recommendation to set custom headers on the Supabase client with the `Authorization` header including your custom JWT. This is no longer recommended as it's less flexible and causes confusion when combined with a user session from Supabase Auth.
## Verifying a JWT from Supabase
If you're not able to use the Supabase client libraries, the following can be used to help you securely verify JWTs issued by Supabase.
Supabase Auth exposes a [JSON Web Key](https://datatracker.ietf.org/doc/html/rfc7517) Set URL for each Supabase project:
```http
GET https://project-id.supabase.co/auth/v1/.well-known/jwks.json
```
Now that you understand what JWTs are and where they're used in Supabase, you can explore how to use them in combination with Row Level Security to start restricting access to certain tables, rows, and columns in your Postgres database.
Which responds with JWKS object containing one or more asymmetric [JWT signing keys](/docs/guides/auth/signing-keys) (only their public keys).
```json
{
"keys": [
{
"kid": "<match with kid from JWT header>",
"alg": "<match with alg from JWT header>",
"kty": "<RSA|EC|OKP>",
"key_ops": ["verify"]
// public key fields
}
]
}
```
This endpoint is served directly from the Auth server, but is also additionally cached by the Supabase Edge for 10 minutes, significantly speeding up access to this data regardless of where you're performing the verification. It's important to be aware of the cache expiry time to prevent unintentionally rejecting valid user access tokens. We recommend waiting at least 20 minutes when creating a standby signing key, or revoking a previously used key.
Make sure that you do not cache this data for longer in your application, as it might make revocation difficult. If you do, make sure to provide a way to purge this cache when rotating signing keys to avoid unintentionally rejecting valid user access tokens.
Below is an example of how to use the [jose TypeScript JWT verification library](https://github.com/panva/jose) with Supabase JWTs:
```typescript
import { jwtVerify, createRemoteJWKSet } from 'jose'
const PROJECT_JWKS = createRemoteJWKSet(
new URL('https://project-id.supabase.co/auth/v1/.well-known/jwks.json')
)
/**
* Verifies the provided JWT against the project's JSON Web Key Set.
*/
async function verifyProjectJWT(jwt: string) {
return jwtVerify(jwt, PROJECT_JWKS)
}
```
### Verifying with the legacy JWT secret or a shared secret signing key
If your project is still using the legacy JWT secret, or you're using a shared secret (HS256) signing key, we recommend always verifying a user access token directly with the Auth server by sending a request like so:
```http
GET https://project-id.supabase.co/auth/v1/user
apikey: publishable or anon legacy API key
Authorization: Bearer <JWT>
```
If the server responds with HTTP 200 OK, the JWT is valid, otherwise it is not.
Because the Auth server runs only in your project's specified region and is not globally distributed, doing this check can be quite slow depending on where you're performing the check. Avoid doing checks like this from servers or functions running on the edge, and prefer routing to a server within the same geographical region as your project.
If you are using the legacy JWT secret, or you've imported your own shared secret (HS256) signing key, you may wish to verify using the shared secret. **We strongly recommend against this approach.**
<Admonition type="caution">
There is almost no benefit from using a JWT signed with a shared secret. Although it's computationally more efficient and verification is simpler to code by hand, using this approach can expose your project's data to significant security vulnerabilities or weaknesses.
Consider the following:
- Using a shared secret can make it more difficult to keep aligned with security compliance frameworks such as SOC2, PCI-DSS, ISO27000, HIPAA, etc.
- A shared secret that is in the hands of a malicious actor can be used to impersonate your users, give them access to privileged actions or data.
- It is difficult to detect or identify when or how a shared secret has been given to a malicious actor.
- Consider who might have even accidental access to the shared secret: systems, staff, devices (and their disk encryption and vulnerability patch status).
- A malicious actor can use a shared secret **far into the future**, so lacking current evidence of compromise does not mean your data is secure.
- It can be very easy to accidentally leak the shared secret in publicly available source code such as in your website or frontend, mobile app package or other executable. This is especially true if you accidentally add the secret in environment variables prefixed with `NEXT_PUBLIC_`, `VITE_`, `PUBLIC_` or other conventions by web frameworks.
- Rotating shared secrets might require careful coordination to avoid downtime of your app.
</Admonition>
Check the JWT verification libraries for your language on how to securely verify JWTs signed with the legacy JWT secret or a shared secret (HS256) signing key. We strongly recommend relying on the Auth server as described above, or switching to a different signing key based on public key cryptography (RSA, Elliptic Curves) instead.
## Resources
- JWT debugger: https://jwt.io/
- [JWT Fields Reference](/docs/guides/auth/jwt-fields) - Complete reference for all JWT fields in Supabase
- [JWT Signing Keys](/docs/guides/auth/signing-keys)
- [JWT Claims Reference](/docs/guides/auth/jwt-fields) - Complete reference for all JWT claims used by Supabase Auth
- [API keys](/docs/guides/api/api-keys)
@@ -0,0 +1,189 @@
---
id: 'auth-signing-keys'
title: 'JWT Signing Keys'
subtitle: 'Best practices on managing keys used by Supabase Auth to create and verify JSON Web Tokens'
---
Supabase Auth continuously issues a new JWT for each user session, for as long as the user remains signed in. JWT signing keys provide fine grained control over this important process for the security of your application.
Before continuing check the comprehensive guide on [Sessions](/docs/guides/auth/sessions) for all the details about how Auth creates tokens for a user's session. Read up on [JWTs](/docs/guides/auth/jwts) if you are not familiar with the basics.
## Overview
When a JWT is issued by Supabase Auth, the key used to create its [signature](https://en.wikipedia.org/wiki/Digital_signature) is known as the signing key. Supabase provides two systems for dealing with signing keys: the Legacy system based on the JWT secret, and the new Signing keys system.
| System | Type | Description |
| ----------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Legacy | JWT&nbsp;secret | Initially Supabase was designed to use a single shared secret key to sign all JWTs. This includes <span className="!whitespace-nowrap">the `anon` and `service_role`</span> keys, all user access tokens including some [Storage pre-signed URLs](/docs/reference/javascript/storage-from-createsignedurl). **No longer recommended.** Available for backward compatibility. |
| Signing&nbsp;keys | Asymmetric&nbsp;key (RSA, Elliptic Curves) | A JWT signing key based on [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) (RSA, Elliptic Curves) that follows industry best practices and significantly improves the security, reliability and performance of your applications. |
| Signing&nbsp;keys | Shared&nbsp;secret&nbsp;key | A JWT signing key based on a [shared secret](https://en.wikipedia.org/wiki/HMAC). |
### Benefits of the signing keys system
We've designed the Signing keys system to address many problems the legacy system had. It goes hand-in-hand with the [publishable and secret API keys](/docs/guides/api/api-keys).
| Benefit | Legacy JWT secret | JWT signing keys |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Performance | Increased app latency as JWT validation is done by Auth server. | If using asymmetric signing key, JWT validation is fast and does not involve Auth server. |
| Reliability | To ensure secure revocation, Auth server is in the hot path of your application. | If using asymmetric signing key, JWT validation is local and fast and does not involve Auth server. |
| Security | Requires changing of your application's backend components to fully revoke a compromised secret. | If using asymmetric signing key, revocation is automatic via the key discovery endpoint. |
| Zero-downtime rotation | Downtime, sometimes being significant. Requires careful coordination with [API keys](/docs/guides/api/api-keys). | No downtime, as each rotation step is independent and reversible. |
| Users signed out during rotation | Currently active users get immediately signed out. | No users get signed out. |
| Independence from API keys | `anon` and `service_role` must be rotated simultaneously. | [Publishable and secret API keys](/docs/guides/api/api-keys) no longer are based on the JWT signing key and can be independently managed. |
| Security compliance frameworks (SOC2, etc.) | Difficult to remain aligned as the secret can be extracted from Supabase. | Easier alignment as the private key or shared secret can't be extracted. [Row Level Security](/docs/guides/database/postgres/row-level-security) has strong key revocation guarantees. |
## Getting started
You can start migrating away from the legacy JWT secret through the Supabase dashboard. This process does not cause downtime for your application.
1. Start off by clicking the _Migrate JWT secret_ button on the [JWT signing keys](/dashboard/project/_/settings/jwt/signing-keys) page. This step will import the existing legacy JWT secret into the new JWT signing keys system. Once this process completes, you will no longer be able to rotate the legacy JWT secret using the old system.
2. Simultaneously, we're creating a new asymmetric JWT signing key for you to rotate to. This key starts off as standby key -- meaning it's being advertised as a key that Supabase Auth will use in the future to create JWTs.
3. If you're not ready to switch away from the legacy JWT secret right now, you can stop here without any issue. If you wish to use a different signing key -- either to use a different signing algorithm (RSA, Elliptic Curve or shared secret) or to import a private key or shared secret you already have -- feel free to move the standby key to _Previously used_ before finally moving it to _Revoked._
4. If you do wish to start using the standby key for all new JWT use the _Rotate keys_ button. A few important notes:
- Make sure your app does not directly rely on the legacy JWT secret. If it's verifying every JWT against the legacy JWT secret (using a library like `jose`, `jsonwebtoken` or similar), continuing with the rotation might break those components.
- If you're using [Edge Functions](/docs/guides/functions) that have the Verify JWT setting, continuing with the rotation might break your app. You will need to turn off this setting.
- In both cases, change or add code to your app or Edge Function that verifies the JWT. Use the `supabase.auth.getClaims()` function or read more about [Verifying a JWT from Supabase](/docs/guides/auth/jwts#verifying-a-jwt-from-supabase) on the best way to do this.
5. Rotating the keys immediately causes the Auth server to issue new JWT access tokens for signed in users signed with the new key. Non-expired access tokens will remain to be accepted, so no users will be forcefully signed out.
6. Plan for revocation of the legacy JWT secret.
- If your access token expiry time is configured to be 1 hour, wait at least 1 hour and 15 minutes before revoking the legacy JWT secret -- now under the _Previously used_ section.
- This prevents currently active users from being forcefully signed out.
- In some situations, such as an active security incident you may want to revoke the legacy JWT secret immediately.
## Rotating and revoking keys
Key rotation and revocation are one of the most important processes for maintaining the security of your project and applications. The signing keys system allows you to efficiently execute these without causing downtime of your app, a deficiency present in the legacy system. Below are some common reasons when and why you should consider key rotation and revocation.
**Malicious actors abusing the legacy JWT secret, or imported private key**
- The legacy JWT secret has been leaked in logs, committed to source control, or accidentally exposed in the frontend build of your application, a library, desktop or mobile app package, etc.
- You suspect that a [member of your organization](/docs/guides/platform/access-control) has lost control of their devices, and a malicious actor may have accessed the JWT secret via the Supabase dashboard or by accessing your application's backend configuration.
- You suspect that an ex-team-member of your organization may be a malicious actor, by abusing the power the legacy JWT secret provides.
- Make sure you also switch to [publishable and secret API keys](/docs/guides/api/api-keys) and disable the `anon` and `service_role` keys.
- If you've imported a private key, and you're suspecting that this private key has been compromised on your end similarly.
**Closer alignment to security best practices and compliance frameworks (SOC2, PCI-DSS, ISO27000, HIPAA, ...)**
- It is always prudent to rotate signing keys at least once a year.
- Some security compliance frameworks strongly encourage or require frequent cryptographic key rotation.
- If you're using Supabase as part of a large enterprise, this may be required by your organization's security department.
- Creating muscle memory for the time you'll need to respond to an active security incident.
**Changing key algorithm for technical reasons**
- You may wish to switch signing algorithms due to compatibility problems or to simplify development on your end.
### Lifetime of a signing key
<div className="flex flex-row gap-6 items-center w-full">
<Image
alt="Diagram showing the state transitions of a signing key"
src={{
light: '/docs/img/guides/auth-signing-keys/states.svg',
dark: '/docs/img/guides/auth-signing-keys/states.svg',
}}
containerClassName="max-w-[300px] min-w-[180px]"
/>
<div>
A newly created key starts off as standby, before being rotated into in use (becoming the current key) while the existing current key becomes previously used.
At any point you can move a key from the previously used or revoked states back to being a standby key, and rotate to it. This gives you the confidence to revert back to an older key if you identify problems with the rotation, such as forgetting to update a component of your application that is relying on a specific key (for example, the legacy JWT secret).
Each action on a key is reversible (except permanent deletion).
</div>
</div>
| Action | Accepted JWT signatures | Description |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <span className="!whitespace-nowrap">Create a new key</span> | Current key only, new key has not created any JWTs yet. | When you initially create a key, after choosing the signing algorithm or importing a private key you already have, it starts out in the standby state. If using an asymmetric key (RSA, Elliptic Curve) its public key will be available in the discovery endpoint. Supabase Auth does not use this key to create new JWTs. |
| <span className="!whitespace-nowrap">Rotate keys</span> | Both keys in the rotation. | Rotation only changes the key used by Supabase Auth to create new JWTs, but the trust relationship with both keys remains. |
| <span className="!whitespace-nowrap">Revoke key</span> | <span className="!whitespace-nowrap">Only from the current key.</span> | Once all regularly valid JWTs have expired (or sooner) revoke the previously used key to revoke trust in it. |
| <span className="!whitespace-nowrap">Move to standby</span> from revoked | Current and previously revoked key. | If you've made a mistake or need more time to adjust your application, you can move a revoked key to standby. Follow up with a rotation to ensure Auth starts using the originally revoked key again to make new JWTs. |
| <span className="!whitespace-nowrap">Move to standby</span> from previously used | Both keys. | This only prepares the key from the last rotation to be used by Auth to make new JWTs with it. |
| <span className="!whitespace-nowrap">Delete key</span> | - | Permanently destroys the private key or shared secret of a key, so it will not be possible to re-use or rotate again into it. |
### Public key discovery and caching
When your signing keys use an asymmetric algorithm based on [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) Supabase Auth exposes the public key in the JSON Web Key Set discovery endpoint, for anyone to see. This is an important security feature allowing you to rotate and revoke keys without needing to deploy new versions of your app's backend infrastructure.
Access the currently trusted signing keys at the following endpoint:
```http
GET https://project-id.supabase.co/auth/v1/.well-known/jwks.json
```
Note that this is secure as public keys are irreversible and can only be used to verify the signature of JSON Web Tokens, but not create new ones.
This discovery endpoint is cached by Supabase's edge servers for 10 minutes. Furthermore the Supabase client libraries may cache the keys in memory for an additional 10 minutes. Your application may be using different caching behavior if you're not relying only on the Supabase client library.
This multi-level cache is a trade-off allowing fast JWT verification without placing the Auth server in the hot path of your application, increasing its reliability and performance.
Importantly Supabase products **do not rely on this cache**, so stronger security guarantees are provided especially when keys are revoked. If your application only uses [Row Level Security](/docs/guides/database/postgres/row-level-security) policies and does not have any other backend components (such as APIs, Edge Functions, servers, etc.) key rotation and revocation are instantaneous.
Finally this multi-level cache is cleared every 20 minutes, or longer if you have a custom setup. Consider the following problems that may arise due to it:
- **Urgent key revocation.** If you are in a security incident where a signing key must be urgently revoked, due to the multi-level cache your application components may still trust and authenticate JWTs signed with the revoked key. Supabase products (Auth, Data API, Storage, Realtime) **do not rely on this cache and revocation is instantaneous.** Should this be an issue for you, ensure you've built a cache busting mechanism as part of your app's backend infrastructure.
- **Quick key creation and rotation.** If you're migrating away from the legacy JWT secret or when only using the `supabase.auth.getClaims()` method this case is handled for you automatically. If you're verifying JWTs on your own, without the help of the Supabase client library, ensure that **all caches in your app** have picked up the newly created standby key before proceeding to rotation.
## Choosing the right signing algorithm
To strike the right balance between performance, security and ease-of-use, JWT signing keys are based on capabilities available in the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).
| Algorithm | <span className="!whitespace-nowrap">JWT `alg`</span> | Information |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <span className="!whitespace-nowrap">[NIST P-256 Curve](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography)</span><br/>(Asymmetric) | <span className="!whitespace-nowrap">`ES256`</span> | Elliptic Curves are a faster alternative than RSA, while providing comparable security. Especially important for Auth use cases is the fact that signatures using the P-256 curve are significantly shorter than those created by RSA, which reduces data transfer sizes and helps in managing cookie size. Web Crypto and most other cryptography libraries and runtimes support this curve. |
| <span className="!whitespace-nowrap">[RSA 2048](https://en.wikipedia.org/wiki/RSA_cryptosystem)</span><br/>(Asymmetric) | <span className="!whitespace-nowrap">`RS256`</span> | RSA is the oldest and most widely supported public-key cryptosystem in use. While being easy to code by hand, it can be significantly slower than elliptic curves in certain aspects. We recommend using the P-256 elliptic curve instead. |
| <span className="!whitespace-nowrap">[Ed25519 Curve](https://en.wikipedia.org/wiki/EdDSA#Ed25519)</span><br/>(Asymmetric) | <span className="!whitespace-nowrap">`EdDSA`</span> | Coming soon. This algorithm is based on a different elliptic curve cryptosystem developed in the open, unlike the P-256 curve. Web Crypto or other crypto libraries may not support it in all runtimes, making it difficult to work with. |
| <span className="!whitespace-nowrap">[HMAC with shared secret](https://en.wikipedia.org/wiki/HMAC)</span><br/>(Symmetric) | <span className="!whitespace-nowrap">`HS256`</span> | **Not recommended for production applications.** A shared secret uses a message authentication code to verify the authenticity of a JSON Web Token. This requires that both the creator of the JWT (Auth) and the system verifying the JWT know the secret. As there is no public key counterpart, revoking this key might require deploying changes to your app's backend infrastructure. |
<Admonition type="caution">
There is almost no benefit from using a JWT signed with a shared secret. Although it's computationally more efficient and verification is simpler to code by hand, using this approach can expose your project's data to significant security vulnerabilities or weaknesses.
Consider the following:
- Using a shared secret can make it more difficult to keep aligned with security compliance frameworks such as SOC2, PCI-DSS, ISO27000, HIPAA, etc.
- A shared secret that is in the hands of a malicious actor can be used to impersonate your users, give them access to privileged actions or data.
- It is difficult to detect or identify when or how a shared secret has been given to a malicious actor.
- Consider who might have even accidental access to the shared secret: systems, staff, devices (and their disk encryption and vulnerability patch status).
- A malicious actor can use a shared secret **far into the future**, so lacking current evidence of compromise does not mean your data is secure.
- It can be very easy to accidentally leak the shared secret in publicly available source code such as in your website or frontend, mobile app package or other executable. This is especially true if you accidentally add the secret in environment variables prefixed with `NEXT_PUBLIC_`, `VITE_`, `PUBLIC_` or other conventions by web frameworks.
- Rotating shared secrets might require careful coordination to avoid downtime of your app.
</Admonition>
## Frequently asked questions
### Why is it not possible to extract the private key or shared secret from Supabase?
You can only extract the legacy JWT secret. Once you've moved to using the JWT signing keys feature extracting of the private key or shared secret from Supabase is not possible. This ensures that no one in your organization is able to impersonate your users or gain privileged access to your project's data.
This guarantee provides your application with close alignment with security compliance frameworks (SOC2, PCI-DSS, ISO27000, HIPAA) and security best practices.
### How to create (mint) JWTs if access to the private key or shared secret is not possible?
If you wish to make your own JWTs or have access to the private key or shared secret used by Supabase, you can create a new JWT signing key by importing a private key or setting a shared secret yourself.
Use the [Supabase CLI](/docs/reference/cli/introduction) to quickly and securely generate a private key ready for import:
```sh
supabase gen generate-key ES256
```
Make sure you store this private key in a secure location, as it will not be extractable from Supabase.
### Why is a 5 minute wait imposed when changing signing key states?
Changing a JWT signing key's state sets off many changes inside the Supabase platform. To ensure a consistent setup, most actions that change the state of a JWT signing key are throttled for approximately 5 minutes.
### Why is deleting the legacy JWT secret disallowed?
This is to ensure you have the ability, should you need it, to go back to the legacy JWT secret. In the future this capability will be allowed from the dashboard.
### Why does revoking the legacy JWT secret require disabling of `anon` and `service_role` API keys?
Unfortunately `anon` and `service_role` are not just API keys, but are also valid JSON Web Tokens, signed by the legacy JWT secret. Revoking the legacy JWT secret means that your application no longer trusts any JWT signed with it. Therefore before you revoke the legacy JWT secret, you must disable the `anon` and `service_role` to ensure a consistent security setup.
@@ -23,6 +23,32 @@ These numbers are generalizations and depends on other Supabase products that yo
### Capturing historical usage
#### Dashboard monitoring charts
<Image
alt="Database client connections chart"
zoomable
src={{
dark: '/docs/img/database/reports/db-connections-chart-dark.png',
light: '/docs/img/database/reports/db-connections-chart-light.png',
}}
/>
For Teams and Enterprise plans, Supabase provides Advanced Telemetry charts directly within the Dashboard. The `Database client connections` chart displays historical connection data broken down by connection type:
- **Postgres**: Direct connections from your application
- **PostgREST**: Connections from the PostgREST API layer
- **Reserved**: Administrative connections for Supabase services
- **Auth**: Connections from Supabase Auth service
- **Storage**: Connections from Supabase Storage service
- **Other roles**: Miscellaneous database connections
This chart helps you monitor connection pool usage, identify connection leaks, and plan capacity. It also shows a reference line for your compute size's maximum connection limit.
For more details on using these monitoring charts, see the [Reports guide](/docs/guides/telemetry/reports#advanced-telemetry).
#### Grafana Dashboard
Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/platform/metrics).
Its "Client Connections" graph displays connections for both Supavisor and Postgres
@@ -1,772 +1,16 @@
---
title: 'Branching'
description: 'Use Supabase Branches to test and preview changes.'
subtitle: 'Use Supabase Branches to test and preview changes'
---
Use branching to safely experiment with changes to your Supabase project.
Supabase branches work like Git branches. They let you create and test changes like new configurations, database schemas, or features in a separate, temporary instance without affecting your production setup.
When you're ready to ship your changes, merge your branch to update your production instance with the new changes.
If you understand Git, you already understand Supabase Branching.
Supabase branches create separate environments that spin off from your main project. You can use these branching environments to create and test changes like new configurations, database schemas, or features without affecting your production setup. When you're ready to ship your changes, merge your branch to update your production instance with the new changes.
## How branching works
- **Separate Environments**: Each branch is a separate environment with its own Supabase instance and API credentials.
- **Git Integration**: Branching works with Git, currently supporting GitHub repositories.
- **Preview Branches**: You can create multiple Preview Branches for testing.
- **Migrations and Seeding**: Branches run migrations from your repository and can seed data using a `seed.sql` file.
## Prerequisites
- **Supabase Project**: You need an existing Supabase project.
- **GitHub Repository**: Your project must be connected to a GitHub repository containing your Supabase directory.
You can run multiple Preview Branches for every Supabase project. Branches contain all the Supabase features with their own API credentials.
Preview Environments auto-pause after <SharedData data="config">branching.inactivity_period_in_minutes</SharedData> minutes of inactivity. Upon receiving a new request to your database or REST API, the paused branch will automatically resume to serve the request. The implications of this architecture means
- `pg_cron` jobs will not execute in an auto-paused database.
- Larger variance in request latency due to database cold starts.
If you need higher performance guarantees on your Preview Environment, you can switch individual branches to [persistent](/docs/guides/deployment/branching#persistent-branches) so they are not auto-paused.
<Image
zoomable
className="max-w-[700px] !mx-auto"
alt="Each branch has a separate Supabase instance."
caption="Each Branch is a separate environment."
src={{
dark: '/docs/img/guides/platform/branching/github-workflow-without-branching.jpg?v=1',
light: '/docs/img/guides/platform/branching/github-workflow-without-branching--light.jpg?v=1',
}}
/>
### Branching workflow
Preview Branch instances contain no data by default. You must include a seed file to seed your preview instance with sample data when the Preview Branch is created. Future versions of Branching may allow for automated data seeding and cloning after we are confident that we can provide safe data masking.
## Git providers
To manage code changes, your Supabase project must be connected to a Git repository. At this stage, we only support [GitHub](#branching-with-github). If you are interested in other Git providers, join the [discussion](https://github.com/orgs/supabase/discussions/18936) for GitLab, Bitbucket, and non-Git based Branching.
### Branching with GitHub
Supabase Branching uses the Supabase GitHub integration to read files from your GitHub repository. With this integration, Supabase watches all commits, branches, and pull requests of your GitHub repository.
You can create a corresponding Preview Branch for any Git branch in your repository. Each time a new Preview Branch is created and configured based on the [`config.toml`](/docs/guides/local-development/cli/config) configuration on this branch, the migrations from the corresponding Git branch are run on the Preview Branch.
The Preview Branch is also [seeded](/docs/guides/local-development/seeding-your-database) with sample data based on `./supabase/seed.sql` by default, if that file exists.
Supabase Branching follows the [Trunk Based Development](https://trunkbaseddevelopment.com/) workflow, with one main Production branch and multiple development branches:
<Image
zoomable
className="max-w-[700px] !mx-auto"
alt="Each GitHub branch can have its own Supabase preview branch."
caption="Each GitHub branch can have its own Supabase preview branch."
src={{
dark: '/docs/img/guides/platform/branching/github-workflow.jpg?v=1',
light: '/docs/img/guides/platform/branching/github-workflow--light.jpg?v=1',
}}
/>
When you merge your Git branch into the production branch, all new migrations will be applied to your Production environment. If you have declared Storage buckets or Edge Functions in `config.toml`, they will also be deployed automatically. All other configurations, including API, Auth, and seed files, will be ignored by default.
<Admonition type="note" label="Data changes are not merged into production." />
### Preparing your Git repository
You can use the [Supabase CLI](/docs/guides/cli) to manage changes inside a local `./supabase` directory:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="existing"
queryGroup="platform"
>
<TabPanel id="existing" label="Existing project">
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Initialize Supabase locally" fullWidth>
If you don't have a `./supabase` directory, you can create one:
```markdown
supabase init
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Pull your database migration" fullWidth>
Pull your database changes using `supabase db pull`. To get your database connection string, go to your project dashboard, click [Connect](https://supabase.com/dashboard/project/_?showConnect=true) and look for the Session pooler connection string.
```markdown
supabase db pull --db-url <db_connection_string>
# Your Database connection string will look like this:
# postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres
```
<Admonition type="note">
If you're in an [IPv6 environment](https://github.com/orgs/supabase/discussions/27034) or have the IPv4 Add-On, you can use the direct connection string instead of Supavisor in Session mode.
</Admonition>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Commit the `supabase` directory to Git" fullWidth>
Commit the `supabase` directory to Git, and push your changes to your remote repository.
```bash
git add supabase
git commit -m "Initial migration"
git push
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
</TabPanel>
<TabPanel id="new" label="New Project">
Use the Next.js example template to try out branching. This template includes sample migration and seed files to get you started. Run the following command in your terminal to clone the example:
```bash
npx create-next-app -e with-supabase
```
Push your new project to a GitHub repo. For more information, see the GitHub guides on [creating](https://docs.github.com/en/get-started/quickstart/create-a-repo) and [pushing code](https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository) to a new repository.
</TabPanel>
</Tabs>
### Enable Supabase branching
Once your repository is [correctly prepared](#preparing-your-git-repository), you can enable branching from the Supabase dashboard.
<Admonition type="caution" label="Prepare your GitHub repository before enabling Branching">
If your repository doesn't have all the migration files, your production branch could run an incomplete set of migrations. Make sure your [GitHub repository is prepared](#preparing-your-git-repository).
</Admonition>
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Inside your Supabase project, click `Enable branching`" fullWidth>
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-opt-in-popover.jpg?v=1" />
</figure>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Install the GitHub integration" fullWidth>
When clicking `Enable branching` you will see the following dialog:
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-opt-in-install-github.jpg?v=1" />
</figure>
If you don't have the GitHub integration installed, click `Add new project connection`. The integration is required to run migration files and the optional database seed file.
You're taken to the GitHub integration page. Click `Install`.
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-opt-in-install-github-permissions.jpg?v=1" />
</figure>
Follow the instructions to link your Supabase project to its GitHub repository.
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-opt-in-install-github-integration-link.jpg?v=1" />
</figure>
Return to your project and re-click `Enable branching`.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="You should now see a popover with the GitHub Connection details shown" fullWidth>
Type in the branch you want to use for production. The name of the branch will be validated to make sure it exists in your GitHub repository.
<Admonition type="caution" label="Your production branch can't be changed while branching is enabled.">
To change your production branch, you need to disable branching and re-enable it with a different branch.
</Admonition>
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-opt-in-install-production-branch.jpg?v=1" />
</figure>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Click `I understand, enable branching`. Branching is now enabled for your project." fullWidth>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
### Open a pull request
When you open a pull request on GitHub, the Supabase integration automatically checks for a matching preview branch. If one doesn't exist, it gets created.
A comment is added to your PR with the deployment status of your preview branch. Statuses are shown separately for Database, Services, and APIs.
<Image
zoomable
className="max-w-[700px] !mx-auto"
alt="GitHub view of the deployment status of your preview branch"
caption="Supabase GitHub integration will comment on your PR with the status of your Preview Branch, including whether migrations have successfully run."
src="/docs/img/guides/platform/branching/develop-your-app-open-pull-request-github.jpg?v=1"
/>
Every time a new commit is pushed that changes the migration files in `./supabase/migrations`, the new migrations are run against the preview branch. You can check the status of these runs in the comment's Tasks table.
### Preventing migration failures
We highly recommend turning on a 'required check' for the Supabase integration. You can do this from your GitHub repository settings. This prevents PRs from being merged when migration checks fail, and stops invalid migrations from being merged into your production branch.
<Image
zoomable
className="max-w-[700px] !mx-auto"
alt='Check the "Require status checks to pass before merging" option.'
caption='Check the "Require status checks to pass before merging" option.'
src="/docs/img/guides/platform/branching/github-required-check.jpg?v=1"
/>
### Manually create a preview branch
Preview branches are automatically created for each pull request, but you can also manually create one.
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Create a new Git branch in your GitHub repository" fullWidth>
You need at least one other branch aside from your Supabase production branch.
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-first-preview-branch-github.jpg?v=1" />
<figcaption>You can use the GitHub dashboard or command line to create a new branch. In this example, the new branch is called `feat/add-members`.</figcaption>
</figure>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Navigate to the Branches page in your Supabase dashboard." fullWidth>
In the Supabase dashboard, look for the branch dropdown on the right-hand side of the top bar. It should be set to your production branch by default. Open the dropdown and click [`Manage branches`](/dashboard/project/_/branches).
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-first-preview-branch-branch-dropdown.jpg?v=1" />
</figure>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Create a Supabase preview branch" fullWidth>
Click `Create preview branch`.
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-first-preview-branch-branch-action.jpg?v=1" />
</figure>
Type in the branch name you want to add. Click `Create branch` to confirm.
<Admonition type="note" label="Only branches from the repository can be used to create a Preview Branch">
Git branches from external contributors currently can't support a Preview Branch
</Admonition>
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/enable-supabase-branching-first-preview-branch-choose-branch.jpg?v=1" />
</figure>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
The Git integration watches for changes in the `supabase` directory. This includes:
- All SQL migration files, under the subdirectory `migrations`
- An optional `seed.sql` file, used to seed preview instances with sample data
You can create new migrations either [locally](#develop-locally) or [remotely](#develop-remotely). Local development is recommended.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="local-dev"
queryGroup="making-updates"
>
<TabPanel id="local-dev" label="Local Development">
The Supabase CLI provides two options: [manual migrations](https://supabase.com/docs/guides/deployment/database-migrations) and [generated migrations](https://supabase.com/docs/guides/deployment/database-migrations#diffing-changes) using Supabase's local studio and the `supabase db-diff` command. Let's use the latter and push the change to our Preview Branch:
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Make schema changes locally" fullWidth>
Start Supabase locally:
```bash
supabase start
```
Then proceed to [localhost:54323](http://localhost:54323) to access your local Supabase dashboard.
You can make changes in either the [Table Editor](http://localhost:54323/project/default/editor) or the [SQL Editor]((http://localhost:54323/project/default/sql)).
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Generate a migration file" fullWidth>
Once you are finished making database changes, run `supabase db diff` to create a new migration file. For example:
```bash
supabase db diff -f "add_employees_table"
```
This will create a SQL file called `./supabase/migrations/[timestamp]add_employees_table.sql`. This file will reflect the changes that you made in your local dashboard.
If you want to continue making changes, you can manually edit this migration file, then use the `db reset` command to pick up your edits:
```bash
supabase db reset
```
This will reset the database and run all the migrations again. The local dashboard at [localhost:54323](http://localhost:54323) will reflect the new changes you made.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Commit your changes and push." fullWidth>
Commit and push your migration file to your remote GitHub repository. For example:
```bash
git add supabase/migrations
git commit -m "Add employees table"
git push --set-upstream origin new-employee
```
The Supabase integration detects the new migration and runs it on the remote Preview Branch. It can take up to 10 minutes for migrations to be applied. If you have a PR for your branch, errors are reflected in the GitHub check run status and in a PR comment.
If you need to reset your database to a clean state (that is, discard any changes that aren't reflected in the migration files), run `supabase db reset` locally. Then, delete the preview branch and recreate it by closing, and reopening your pull request.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
</TabPanel>
<TabPanel id="remote-dev" label="Remote Development">
As an alternative to developing locally, you can make changes on your remote Supabase dashboard. You can then pull these changes to your local machine and commit them to GitHub.
<Admonition type="note" label="Changes must be locally pulled and committed to keep your Git repository state in sync.">
Dashboard changes aren't automatically reflected in your Git repository. If you'd like to see automatic syncing in a future release, [join the discussion](https://github.com/orgs/supabase/discussions/18937).
</Admonition>
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Select the branch you want to use." fullWidth>
Select the branch you wish to use in your Supabase [project](https://supabase.com/dashboard/project/_).
<figure className="max-w-[520px]">
<Image src="/docs/img/guides/platform/branching/develop-your-app-develop-remotely.jpg?v=1" />
</figure>
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Make changes to your database schema." fullWidth>
Make changes to your schema with either the [Table Editor](https://supabase.com/dashboard/project/_/editor) or the [SQL Editor]((https://supabase.com/dashboard/project/_/sql)).
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Check you have the password for the preview branch database." fullWidth>
If you don't know the password, you must reset the database password so you know the password. Go to the [database setting page](https://supabase.com/dashboard/project/_/settings/database) and click `Reset database password`.
Save the new password securely for future use.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Pull changes from remote database." fullWidth>
In your local terminal, navigate to the directory for your Supabase project and use the Supabase CLI to pull changes from your branch to your local migrations directory.
Make sure to use the database URL for your branch:
```bash
supabase db pull --db-url "postgres://postgres.xxxx:password@xxxx.pooler.supabase.com:6543/postgres"
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Details title="Commit and push your migration file." fullWidth>
No new migrations will be run on your remote Preview Branch after pushing your changes. This is expected, because your database is already up-to-date, based on the changes you made in the dashboard. But this ensures that your migration files are in GitHub, so they can be correctly merged into production.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
</TabPanel>
</Tabs>
### Disable branching
You can disable branching at any time. Navigate to the [Branches](/dashboard/project/_/branches) page, which can be found via the Branches dropdown menu on the top navigation, then click "Manage Branches" in the menu. Click the 'Disable branching' button at the top of the Overview section.
### Persistent branches
Persistent branches are the type of branches that will remain active even after the underlying PR is closed. Any PR based on a persistent branch will also have a corresponding preview branch created automatically.
You can change any branch to be persistent on the [Branches](/dashboard/project/_/branches) page by clicking the triple dots icon next to the branch you want to modify, and selecting "Switch to persistent".
All persistent branches can be toggled back to be an ephemeral branch in the exact same way.
## Migration and seeding behavior
Migrations are run in sequential order. Each migration builds upon the previous one.
The preview branch has a record of which migrations have been applied, and only applies new migrations for each commit. This can create an issue when rolling back migrations.
### Using ORM or custom seed scripts
If you want to use your own ORM for managing migrations and seed scripts, you will need to run them in GitHub Actions after the preview branch is ready. The branch credentials can be fetched using the following example GHA workflow.
```yaml
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- main
paths:
- 'supabase/**'
jobs:
wait:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.conclusion }}
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase Preview
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
migrate:
needs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: supabase/setup-cli@v1
with:
version: latest
- run: supabase --experimental branches get "$GITHUB_HEAD_REF" -o env >> $GITHUB_ENV
- name: Custom ORM migration
run: psql "$POSTGRES_URL_NON_POOLING" -c 'select 1'
```
## Branch configuration with remotes
When Branching is enabled, your `config.toml` settings automatically sync to all ephemeral branches through a one-to-one mapping between your Git and Supabase branches.
### Basic configuration
To update configuration for a Supabase branch, modify `config.toml` and push to git. The Supabase integration will detect the changes and apply them to the corresponding branch.
### Remote-specific configuration
For persistent branches that need specific settings, you can use the `[remotes]` block in your `config.toml`. Each remote configuration must reference an existing project ID.
Here's an example of configuring a separate seed script for a staging environment:
```toml
[remotes.staging]
project_id = "your-project-ref"
[remotes.staging.db.seed]
sql_paths = ["./seeds/staging.sql"]
```
Since the `project_id` field must reference an existing branch, you need to create the persistent branch before adding its configuration. Use the CLI to create a persistent branch first:
```bash
supabase --experimental branches create --persistent
# Do you want to create a branch named develop? [Y/n]
```
### Configuration merging
When merging a PR into a persistent branch, the Supabase integration:
1. Checks for configuration changes
2. Logs the changes
3. Applies them to the target remote
If no remote is declared or the project ID is incorrect, the configuration step is skipped.
### Available configuration options
All standard configuration options are available in the `[remotes]` block. This includes:
- Database settings
- API configurations
- Authentication settings
- Edge Functions configuration
- And more
You can use this to maintain different configurations for different environments while keeping them all in version control.
### Managing secrets for branches
For sensitive configuration like SMTP credentials or API keys, you can use the Supabase CLI to manage secrets for your branches. This is especially useful for custom SMTP setup or other services that require secure credentials.
To set secrets for a persistent branch:
```bash
# Set secrets from a .env file
supabase secrets set --env-file ./supabase/.env
# Or set individual secrets
supabase secrets set SMTP_HOST=smtp.example.com
supabase secrets set SMTP_USER=your-username
supabase secrets set SMTP_PASSWORD=your-password
```
These secrets will be available to your branch's services and can be used in your configuration. For example, in your `config.toml`:
```toml
[auth.smtp]
host = "env(SMTP_HOST)"
user = "env(SMTP_USER)"
password = "env(SMTP_PASSWORD)"
```
<Admonition type="note" label="Secrets are branch-specific">
Secrets set for one branch are not automatically available in other branches. You'll need to set them separately for each branch that needs them.
</Admonition>
#### Using dotenvx for git-based workflow
For managing environment variables across different branches, you can use [dotenvx](https://dotenvx.com/) to securely manage your configurations. This approach is particularly useful for teams working with Git branches and preview deployments.
##### Environment file structure
Following the conventions used in the [example repository](https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md), environments are configured using dotenv files in the `supabase` directory:
| File | Environment | `.gitignore` it? | Encrypted |
| --------------- | ----------- | ---------------- | --------- |
| .env.keys | All | Yes | No |
| .env.local | Local | Yes | No |
| .env.production | Production | No | Yes |
| .env.preview | Branches | No | Yes |
| .env | Any | Maybe | Yes |
##### Setting up encrypted secrets
1. Generate key pair and encrypt your secrets:
```bash
npx @dotenvx/dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "<your-secret>" -f supabase/.env.preview
```
This creates a new encryption key in `supabase/.env.preview` and a new decryption key in `supabase/.env.keys`.
2. Update project secrets:
```bash
npx supabase secrets set --env-file supabase/.env.keys
```
3. Choose your configuration approach in `config.toml`:
Option A: Use encrypted values directly:
```toml
[auth.external.github]
enabled = true
secret = "encrypted:<encrypted-value>"
```
Option B: Use environment variables:
```toml
[auth.external.github]
enabled = true
client_id = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"
```
<Admonition type="note" label="Secret fields">
The `encrypted:` syntax only works for designated "secret" fields in the configuration (like `secret` in auth providers). Using encrypted values in other fields will not be automatically decrypted and may cause issues. For non-secret fields, use environment variables with the `env()` syntax instead.
</Admonition>
##### Using with preview branches
When you commit your `.env.preview` file with encrypted values, the branching executor will automatically retrieve and use these values when deploying your branch. This allows you to maintain different configurations for different branches while keeping sensitive information secure.
### Rolling back migrations
You might want to roll back changes you've made in an earlier migration change. For example, you may have pushed a migration file containing schema changes you no longer want.
To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it.
The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order.
### Seeding behavior
Your Preview Branches are seeded with sample data using the same as [local seeding behavior](/docs/guides/local-development/seeding-your-database).
The database is only seeded once, when the preview branch is created. To rerun seeding, delete the preview branch and recreate it by closing, and reopening your pull request.
## Branching and hosting providers
Branching works with hosting providers that support preview deployments.
With the Supabase branching integration, you can sync the Git branch used by the hosting provider with the corresponding Supabase preview branch. This means that the preview deployment built by your hosting provider is matched to the correct database schema, edge functions, and other Supabase configurations.
### Vercel
Install the Vercel integration:
- From the [Vercel marketplace](https://vercel.com/integrations/supabase) or
- By clicking the blue `Deploy` button in a Supabase example app's `README` file
<Admonition type="note" label="Vercel GitHub integration also required.">
For branching to work with Vercel, you also need the [Vercel GitHub integration](https://vercel.com/docs/deployments/git/vercel-for-github).
</Admonition>
And make sure you have [connected](/dashboard/org/_/integrations) your Supabase project to your Vercel project.
Supabase automatically updates your Vercel project with the correct environment variables for the corresponding preview branches. The synchronization happens at the time of Pull Request being opened, not at the time of branch creation.
As branching integration is tied to the Preview Deployments feature in Vercel, there are possible race conditions between Supabase setting correct variables, and Vercel running a deployment process. Because of that, Supabase is always automatically re-deploying the most recent deployment of the given pull request.
## Other Git providers
There are multiple alternative Git providers under consideration. If you're interested in branching for GitLab, Bitbucket, or some other provider, [join the GitHub discussion](https://github.com/orgs/supabase/discussions/18938).
## Alternatives to branching
Under the hood, you can see Supabase branching as a way to programmatically "duplicate" your Supabase project via git flow. This allows spawning a new configured (via [`config.toml`](/docs/guides/local-development/cli/config)) and seeded instance of the database and the adjacent Supabase services (buckets, edge functions, etc.).
1. A new project is deployed on behalf of the user on the Supabase side as the new "branch" if it doesn't already exist. This includes the database, storage, edge-function, and all Supabase-related services.
2. The branch is cloned and the new project is configured based on the [`config.toml`](/docs/guides/local-development/cli/config) committed into this project branch.
3. Migrations are applied and seeding scripts are run (the first time) for this branch.
You can make a similar setup with a distinct project for each environment. Or just have two environments, the localhost and the production one.
## Pricing
Branching is available on the Pro Plan and above. The price is:
- Each Preview branch costs <Price price="0.01344" /> per hour
- Each Preview branch is billed until it is removed
## Troubleshooting
### Rolling back migrations
You might want to roll back changes you've made in an earlier migration change. For example, you may have pushed a migration file containing schema changes you no longer want.
To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it.
The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order.
### Deployment failures
A deployment might fail for various reasons, including invalid SQL statements and schema conflicts in migrations, errors within the `config.toml` config, or something else.
To check the error message, see the Supabase workflow run for your branch under the [View logs](/dashboard/project/_/branches) section.
### Network restrictions
If you enable [network restrictions](/docs/guides/platform/network-restrictions) on your project, the branching cluster will be blocked from connecting to your project by default. This often results in database connection failures when migrating your production project after merging a development branch.
The workaround is to explicitly allow the IPv6 CIDR range of the branching cluster in your project's [database settings](https://supabase.com/dashboard/project/_/settings/database) page: `2600:1f18:2b7d:f600::/56`
<Image
alt="Network restrictions to allow connections from branching cluster"
src={{
dark: '/docs/img/guides/branching/cidr-dark.png',
light: '/docs/img/guides/branching/cidr-light.png',
}}
className="max-w-[550px] !mx-auto border rounded-md"
zoomable
/>
### Schema drift between preview branches
If multiple preview branches exist, each preview branch might contain different schema changes. This is similar to Git branches, where each branch might contain different code changes.
When a preview branch is merged into the production branch, it creates a schema drift between the production branch and the preview branches that haven't been merged yet.
These conflicts can be resolved in the same way as normal Git Conflicts: merge or rebase from the production Git branch to the preview Git branch. Since migrations are applied sequentially, ensure that migration files are timestamped correctly after the rebase. Changes that build on top of earlier changes should always have later timestamps.
### Changing production branch
It's not possible to change the Git branch used as the Production branch for Supabase Branching. The only way to change it is to disable and re-enable branching. See [Disable Branching](#disable-branching).
## Feedback
Supabase branching is a new and exciting part of the Supabase development ecosystem. Feedback is welcome.
You can join the [conversation over in GitHub discussions](https://github.com/orgs/supabase/discussions/18937).
- **Persistent Branches**: Persistent branches are long-lived branches. They aren't automatically paused or deleted due to non-inactivity or merging.
- **GitHub integration and Branching 2.0**: You can use branching with the GitHub integration, or use Branching 2.0 to manage your branches without Git (in Feature Preview).
- **Data-less**: New branches do not start with any data from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration.
@@ -0,0 +1,67 @@
---
title: 'Branching 2.0 (Alpha)'
subtitle: 'Create and manage branches without using Git'
---
Branching 2.0 allows you to create and manage Supabase branches without connecting to a Git repository. This is useful for quick testing, prototyping, or when you prefer to manage your database changes outside of Git.
<Admonition type="note" label="Public Alpha">
Branching 2.0 is currently in public alpha. Features and functionality may change.
</Admonition>
## How Branching 2.0 works
With Branching 2.0, you can do the following directly from the Supabase dashboard:
- Create preview branches
- Make changes to your public schema or edge functions
- Merge these changes back into production when ready
- Pull in updates from production
## Enable Branching 2.0
Branching 2.0 is available as a Feature Preview. To enable Branching 2.0 in the Supabase Dashboard:
1. Open the user menu by clicking on your user icon in the top right.
1. Select **Branching 2.0**.
1. Click **Enable feature**.
## Creating a branch
Once you've enabled Branching 2.0, you can create a new branch:
1. Click the arrows next to the branch name in the top menu bar. (The top menu bar has the format `YOUR_ORGANIZATION / YOUR_PROJECT / CURRENT_BRANCH_NAME`.)
2. Click `Create branch`.
## Making changes to a branch
Use the branch selector in the top bar to change to your branch. Any changes you make (including SQL run in the SQL editor, table editor changes, and configuration changes) are now made against the currently selected branch.
You can also use the branch's API keys and connection strings to run changes against the branch from your own code or SQL client.
## Creating a merge request
To review and merge changes from a branch back into your production branch, you must first create a merge request. There are two ways to do this.
The first is to click the merge request button next to the branch selector that's located in the top menu. This will create the merge request and redirect you to the merge page where you can review and merge any changes.
The second is to click on manage branches from within the branch selector, then in the left hand navigation you can click on merge requests. From here you can view all open merge requests and create new ones.
## Pulling changes from production into a branch
When reviewing a merge request you may see a notice at the top of the page asking you to update your branch. This appears when your preview branch has drifted from your production branch. There may be public schema or edge function changes that have been made after your preview branch was created. Clicking update branch will attempt to pull in these changes, but be aware that by doing this your existing edge functions will be replaced. Any new edge functions created on the preview branch will remain untouched.
## Limitations
There are a few limitations you should be aware of before deciding to use branching without git.
- Custom roles created through the dashboard are not captured on branch creation
- Only public schema changes are supported right now
- Extensions are not included in the diff process
- Branches can only be merged to main; merging between preview branches is not supported
- If your branch is out of date, you can pull in latest changes from main, but keep in mind that all functions will be overwritten
- Deleting functions must be done manually on main branch
- Migration conflicts must be manually resolved on the preview branch
- If you have run migrations on main, new branches will be created from existing migrations instead of a full schema dump
@@ -0,0 +1,207 @@
---
title: 'Configuration'
subtitle: 'Configure your Supabase branches using configuration as code'
---
This guide covers how to configure your Supabase branches, using the `config.toml` file. In one single file, you can configure all your branches, including branch settings and secrets.
## Branch configuration with remotes
When Branching is enabled, your `config.toml` settings automatically sync to all ephemeral branches through a one-to-one mapping between your Git and Supabase branches.
### Basic configuration
To update configuration for a Supabase branch, modify `config.toml` and push to git. The Supabase integration will detect the changes and apply them to the corresponding branch.
### Remote-specific configuration
For persistent branches that need specific settings, you can use the `[remotes]` block in your `config.toml`. Each remote configuration must reference an existing project ID.
Here's an example of configuring a separate seed script for a staging environment:
```toml
[remotes.staging]
project_id = "your-project-ref"
[remotes.staging.db.seed]
sql_paths = ["./seeds/staging.sql"]
```
Since the `project_id` field must reference an existing branch, you need to create the persistent branch before adding its configuration. Use the CLI to create a persistent branch first:
```bash
supabase --experimental branches create --persistent
# Do you want to create a branch named develop? [Y/n]
```
### Configuration merging
When merging a PR into a persistent branch, the Supabase integration:
1. Checks for configuration changes
2. Logs the changes
3. Applies them to the target remote
If no remote is declared or the project ID is incorrect, the configuration step is skipped.
### Available configuration options
All standard configuration options are available in the `[remotes]` block. This includes:
- Database settings
- API configurations
- Authentication settings
- Edge Functions configuration
- And more
You can use this to maintain different configurations for different environments while keeping them all in version control.
## Managing secrets for branches
For sensitive configuration like SMTP credentials or API keys, you can use the Supabase CLI to manage secrets for your branches. This is especially useful for custom SMTP setup or other services that require secure credentials.
To set secrets for a persistent branch:
```bash
# Set secrets from a .env file
supabase secrets set --env-file ./supabase/.env
# Or set individual secrets
supabase secrets set SMTP_HOST=smtp.example.com
supabase secrets set SMTP_USER=your-username
supabase secrets set SMTP_PASSWORD=your-password
```
These secrets will be available to your branch's services and can be used in your configuration. For example, in your `config.toml`:
```toml
[auth.smtp]
host = "env(SMTP_HOST)"
user = "env(SMTP_USER)"
password = "env(SMTP_PASSWORD)"
```
<Admonition type="note" label="Secrets are branch-specific">
Secrets set for one branch are not automatically available in other branches. You'll need to set them separately for each branch that needs them.
</Admonition>
### Using dotenvx for git-based workflow
For managing environment variables across different branches, you can use [dotenvx](https://dotenvx.com/) to securely manage your configurations. This approach is particularly useful for teams working with Git branches and preview deployments.
#### Environment file structure
Following the conventions used in the [example repository](https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md), environments are configured using dotenv files in the `supabase` directory:
| File | Environment | `.gitignore` it? | Encrypted |
| --------------- | ----------- | ---------------- | --------- |
| .env.keys | All | Yes | No |
| .env.local | Local | Yes | No |
| .env.production | Production | No | Yes |
| .env.preview | Branches | No | Yes |
| .env | Any | Maybe | Yes |
#### Setting up encrypted secrets
1. Generate key pair and encrypt your secrets:
```bash
npx @dotenvx/dotenvx set SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET "<your-secret>" -f supabase/.env.preview
```
This creates a new encryption key in `supabase/.env.preview` and a new decryption key in `supabase/.env.keys`.
2. Update project secrets:
```bash
npx supabase secrets set --env-file supabase/.env.keys
```
3. Choose your configuration approach in `config.toml`:
Option A: Use encrypted values directly:
```toml
[auth.external.github]
enabled = true
secret = "encrypted:<encrypted-value>"
```
Option B: Use environment variables:
```toml
[auth.external.github]
enabled = true
client_id = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"
```
<Admonition type="note" label="Secret fields">
The `encrypted:` syntax only works for designated "secret" fields in the configuration (like `secret` in auth providers). Using encrypted values in other fields will not be automatically decrypted and may cause issues. For non-secret fields, use environment variables with the `env()` syntax instead.
</Admonition>
#### Using with preview branches
When you commit your `.env.preview` file with encrypted values, the branching executor will automatically retrieve and use these values when deploying your branch. This allows you to maintain different configurations for different branches while keeping sensitive information secure.
## Configuration examples
### Multi-environment setup
Here's an example of a complete multi-environment configuration:
```toml
# Default configuration for all branches
[api]
enabled = true
port = 54321
schemas = ["public", "storage", "graphql_public"]
[db]
port = 54322
pool_size = 10
# Staging-specific configuration
[remotes.staging]
project_id = "staging-project-ref"
[remotes.staging.api]
max_rows = 1000
[remotes.staging.db.seed]
sql_paths = ["./seeds/staging.sql"]
# Production-specific configuration
[remotes.production]
project_id = "prod-project-ref"
[remotes.production.api]
max_rows = 500
[remotes.production.db]
pool_size = 25
```
### Feature branch configuration
For feature branches that need specific settings:
```toml
[remotes.feature-oauth]
project_id = "feature-branch-ref"
[remotes.feature-oauth.auth.external.google]
enabled = true
client_id = "env(GOOGLE_CLIENT_ID)"
secret = "env(GOOGLE_CLIENT_SECRET)"
```
## Next steps
- Explore [branching integrations](/docs/guides/deployment/branching/integrations)
- Learn about [troubleshooting branches](/docs/guides/deployment/branching/troubleshooting)
- Review [branching pricing](/docs/guides/deployment/branching/pricing)
@@ -0,0 +1,102 @@
---
title: 'GitHub integration'
subtitle: 'Connect with GitHub to sync branches with your repository'
---
Supabase Branching uses the Supabase GitHub integration to read files from your GitHub repository. With this integration, Supabase watches all commits, branches, and pull requests of your GitHub repository.
## Installation
In the Supabase Dashboard:
1. Go to **Project Settings** > [**Integrations**](/dashboard/project/_/settings/integrations).
2. Under **GitHub Integration**, click **Authorize GitHub**.
3. You are redirected to a GitHub authorization page. Click **Authorize Supabase**.
4. You are redirected back to the Integrations page. Choose a GitHub repository to connect your project to.
5. Fill in the relative path to the Supabase directory from your repository root.
6. Configure the other options as needed to automate your GitHub connection.
7. Click **Enable integration**.
## Syncing GitHub branches
Enable the **Automatic branching** option in your GitHub Integration configuration to automatically sync GitHub branches with Supabase branches.
When a new branch is created in GitHub, a corresponding branch is created in Supabase. (You can enable the **Supabase changes only** option to only create Supabase branches when Supabase files change.)
### Configuration
You can test configuration changes on your Preview Branch by configuring the `config.toml` file in your Supabase directory. See the [Configuration docs](/docs/guides/deployment/branching/configuration) for more information.
A comment is added to your PR with the deployment status of your preview branch.
### Migrations
The migrations in the `migrations` subdirectory of your Supabase directory are automatically run.
### Seeding
No production data is copied to your Preview branch. This is meant to protect your sensitive production data.
You can seed your Preview Branch with sample data using the `seed.sql` file in your Supabase directory. See the [Seeding docs](/docs/guides/local-development/seeding-your-database) for more information.
Data changes in your seed files are not merged to production.
## Deploying changes to production
Enable the **Deploy to production** option in your GitHub Integration configuration to automatically deploy changes when you push or merge to production branch.
The following changes are deployed:
- New migrations are applied
- Edge Functions declared in `config.toml` are deployed
- Storage buckets declared in `config.toml` are deployed
All other configurations, including API, Auth, and seed files, are ignored by default.
## Preventing migration failures
We highly recommend turning on a 'required check' for the Supabase integration. You can do this from your GitHub repository settings. This prevents PRs from being merged when migration checks fail, and stops invalid migrations from being merged into your production branch.
<Image
zoomable
className="max-w-[700px] !mx-auto"
alt='Check the "Require status checks to pass before merging" option.'
caption='Check the "Require status checks to pass before merging" option.'
src="/docs/img/guides/platform/branching/github-required-check.jpg?v=1"
/>
### Email notifications
To catch failures early, we also recommend subscribing to email notifications on your branch. Common errors include migration conflict, function deployment failure, or invalid configuration file.
You can setup a custom GitHub Action to monitor the status of any Supabase Branch.
```yaml name=.github/workflows/notify-failure.yaml
name: Branch Status
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- main
- develop
paths:
- 'supabase/**'
jobs:
failed:
runs-on: ubuntu-latest
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase Preview
ref: ${{ github.event.pull_request.head.sha || github.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- if: ${{ steps.check.outputs.conclusion == 'failure' }}
run: exit 1
```
@@ -0,0 +1,30 @@
---
title: 'Integrations'
description: 'Use Supabase branching with hosting providers and other tools.'
subtitle: 'Use Supabase branching with hosting providers and other tools'
---
Branching works with hosting providers that support preview deployments. Learn how to integrate Supabase branching with various platforms and tools.
## Hosting providers
With the Supabase branching integration, you can sync the Git branch used by the hosting provider with the corresponding Supabase preview branch. This means that the preview deployment built by your hosting provider is matched to the correct database schema, edge functions, and other Supabase configurations.
### Vercel
Install the Vercel integration:
- From the [Vercel marketplace](https://vercel.com/integrations/supabase) or
- By clicking the blue `Deploy` button in a Supabase example app's `README` file
<Admonition type="note" label="Vercel GitHub integration also required.">
For branching to work with Vercel, you also need the [Vercel GitHub integration](https://vercel.com/docs/deployments/git/vercel-for-github).
</Admonition>
And make sure you have [connected](/dashboard/org/_/integrations) your Supabase project to your Vercel project.
Supabase automatically updates your Vercel project with the correct environment variables for the corresponding preview branches. The synchronization happens at the time of Pull Request being opened, not at the time of branch creation.
As branching integration is tied to the Preview Deployments feature in Vercel, there are possible race conditions between Supabase setting correct variables, and Vercel running a deployment process. Because of that, Supabase is always automatically re-deploying the most recent deployment of the given pull request.
@@ -0,0 +1,169 @@
---
title: 'Troubleshooting'
description: 'Common issues and solutions for Supabase branching.'
subtitle: 'Common issues and solutions for Supabase branching'
---
This guide covers common issues you might encounter when using Supabase branching and how to resolve them.
## Common issues
### Rolling back migrations
You might want to roll back changes you've made in an earlier migration change. For example, you may have pushed a migration file containing schema changes you no longer want.
To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it.
The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order.
### Deployment failures
A deployment might fail for various reasons, including invalid SQL statements and schema conflicts in migrations, errors within the `config.toml` config, or something else.
To check the error message, see the Supabase workflow run for your branch under the [View logs](/dashboard/project/_/branches) section.
### Network restrictions
If you enable [network restrictions](/docs/guides/platform/network-restrictions) on your project, the branching cluster will be blocked from connecting to your project by default. This often results in database connection failures when migrating your production project after merging a development branch.
The workaround is to explicitly allow the IPv6 CIDR range of the branching cluster in your project's [database settings](https://supabase.com/dashboard/project/_/settings/database) page: `2600:1f18:2b7d:f600::/56`
<Image
alt="Network restrictions to allow connections from branching cluster"
src={{
dark: '/docs/img/guides/branching/cidr-dark.png',
light: '/docs/img/guides/branching/cidr-light.png',
}}
className="max-w-[550px] !mx-auto border rounded-md"
zoomable
/>
### Schema drift between preview branches
If multiple preview branches exist, each preview branch might contain different schema changes. This is similar to Git branches, where each branch might contain different code changes.
When a preview branch is merged into the production branch, it creates a schema drift between the production branch and the preview branches that haven't been merged yet.
These conflicts can be resolved in the same way as normal Git Conflicts: merge or rebase from the production Git branch to the preview Git branch. Since migrations are applied sequentially, ensure that migration files are timestamped correctly after the rebase. Changes that build on top of earlier changes should always have later timestamps.
### Changing production branch
It's not possible to change the Git branch used as the Production branch for Supabase Branching. The only way to change it is to disable and re-enable branching. See [Disable Branching](#disable-branching).
## Migration issues
### Failed migrations
When migrations fail, check:
1. **SQL syntax**: Ensure your migration files contain valid SQL
2. **Dependencies**: Check if migrations depend on objects that don't exist
3. **Permissions**: Verify the migration doesn't require superuser privileges
To debug:
```bash
# Test migrations locally first
supabase db reset
# Check migration logs in the dashboard
# Navigate to Branches > Your Branch > View Logs
```
### Migration order problems
Migrations must run in the correct order. Common issues:
1. **Timestamp conflicts**: Ensure migration files have unique timestamps
2. **Dependency issues**: Later migrations depending on earlier ones
3. **Rebase problems**: Timestamps getting out of order after Git rebase
Fix by:
```bash
# Rename migration files to fix timestamp order
mv 20240101000000_old.sql 20240102000000_old.sql
# Reset local database to test
supabase db reset
```
## Connection issues
### Cannot connect to preview branch
If you can't connect to a preview branch:
1. **Check credentials**: Ensure you're using the correct branch-specific credentials
2. **Auto-pause**: The branch might be paused. It will resume on the first request
3. **Network restrictions**: Check if network restrictions are blocking access
### Connection timeouts
Preview branches auto-pause after inactivity. First connections after pause may timeout:
1. **Retry**: The branch will wake up after the first request
2. **Persistent branches**: Convert frequently-used branches to persistent
## Configuration problems
### Config.toml not applying
If configuration changes aren't applying:
1. **Syntax errors**: Validate your `config.toml` syntax
2. **Git sync**: Ensure changes are committed and pushed
3. **Branch refresh**: Try deleting and recreating the branch
### Secrets not available
If secrets aren't working in your branch:
1. **Branch-specific**: Remember secrets are set per branch
2. **Syntax**: Use correct syntax: `env(SECRET_NAME)`
3. **CLI version**: Ensure you're using the latest CLI version
## Performance issues
### Slow branch creation
Branch creation might be slow due to:
1. **Large migrations**: Many or complex migration files
2. **Seed data**: Large seed files take time to process
3. **Network latency**: Geographic distance from the branch region
### Query performance
Preview branches may have different performance characteristics:
1. **Cold starts**: First queries after auto-pause are slower
2. **Resource limits**: Preview branches have different resource allocations
3. **Indexing**: Ensure proper indexes exist in your migrations
## Data issues
### Seed data not loading
If seed data isn't loading:
1. **File location**: Ensure `seed.sql` is in `./supabase/` directory
2. **SQL errors**: Check for syntax errors in seed file
3. **Dependencies**: Seed data might reference non-existent tables
### Data persistence
Remember that preview branch data:
1. **Is temporary**: Data is lost when branch is deleted
2. **Isn't migrated**: Data doesn't move between branches
3. **Resets on recreation**: Deleting and recreating branch loses data
## Getting help
If you're still experiencing issues:
1. **Check logs**: Review branch logs in the dashboard
2. **Community**: Ask in [GitHub discussions](https://github.com/orgs/supabase/discussions/18937)
3. **Support**: Contact support for project-specific issues
4. **Documentation**: Review the latest documentation for updates
@@ -0,0 +1,128 @@
---
title: 'Working with branches'
description: 'Learn how to develop and manage your Supabase branches.'
subtitle: 'Learn how to develop and manage your Supabase branches'
---
This guide covers how to work with Supabase branches effectively, including migration management, seeding behavior, and development workflows.
## Migration and seeding behavior
Migrations are run in sequential order. Each migration builds upon the previous one.
The preview branch has a record of which migrations have been applied, and only applies new migrations for each commit. This can create an issue when rolling back migrations.
### Using ORM or custom seed scripts
If you want to use your own ORM for managing migrations and seed scripts, you will need to run them in GitHub Actions after the preview branch is ready. The branch credentials can be fetched using the following example GHA workflow.
```yaml name=.github/workflows/custom-orm.yaml
name: Custom ORM
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- main
paths:
- 'supabase/**'
jobs:
wait:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.conclusion }}
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase Preview
ref: ${{ github.event.pull_request.head.sha || github.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
migrate:
needs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: supabase/setup-cli@v1
with:
version: latest
- run: supabase --experimental branches get "$GITHUB_HEAD_REF" -o env >> $GITHUB_ENV
- name: Custom ORM migration
run: psql "$POSTGRES_URL_NON_POOLING" -c 'select 1'
```
### Rolling back migrations
You might want to roll back changes you've made in an earlier migration change. For example, you may have pushed a migration file containing schema changes you no longer want.
To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it.
The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order.
### Seeding behavior
Your Preview Branches are seeded with sample data using the same as [local seeding behavior](/docs/guides/local-development/seeding-your-database).
The database is only seeded once, when the preview branch is created. To rerun seeding, delete the preview branch and recreate it by closing, and reopening your pull request.
## Developing with branches
You can develop with branches using either local or remote development workflows.
### Local development workflow
1. Create a new Git branch for your feature
2. Make schema changes using the Supabase CLI
3. Generate migration files with `supabase db diff`
4. Test your changes locally
5. Commit and push to GitHub
6. Open a pull request to create a preview branch
### Remote development workflow
1. Create a preview branch in the Supabase dashboard
2. Switch to the branch using the branch dropdown
3. Make schema changes in the dashboard
4. Pull changes locally using `supabase db pull`
5. Commit the generated migration files
6. Push to your Git repository
## Managing branch environments
### Switching between branches
Use the branch dropdown in the Supabase dashboard to switch between different branches. Each branch has its own:
- Database instance
- API endpoints
- Authentication settings
- Storage buckets
### Accessing branch credentials
Each branch has unique credentials that you can find in the dashboard:
1. Switch to your desired branch
2. Navigate to Settings > API
3. Copy the branch-specific URLs and keys
### Branch isolation
Branches are completely isolated from each other. Changes made in one branch don't affect others, including:
- Database schema and data
- Storage objects
- Edge Functions
- Auth configurations
## Next steps
- Learn about [branch configuration](/docs/guides/deployment/branching/configuration)
- Explore [integrations](/docs/guides/deployment/branching/integrations)
- Review [troubleshooting guide](/docs/guides/deployment/branching/troubleshooting)
@@ -27,6 +27,12 @@ Develop locally while running the Supabase stack on your machine.
pnpm add supabase --save-dev --allow-build=supabase
```
<Admonition type="note">
The `--allow-build=supabase` flag is required on pnpm version 10 or higher. If you're using an older version of pnpm, omit this flag.
</Admonition>
</TabPanel><TabPanel id="brew" label="brew">
```sh
@@ -42,12 +42,6 @@ create table "employees" (
</StepHikeCompact.Step>
</StepHikeCompact>
<Admonition type="note">
Make sure your local database is stopped before diffing your schema.
</Admonition>
<StepHikeCompact>
<StepHikeCompact.Step step={2}>
@@ -9,8 +9,6 @@ Supabase provides multi-factor authentication (MFA) enforcement on the organizat
MFA enforcement is only available on the [Pro, Team and Enterprise plans](https://supabase.com/pricing).
This feature is currently in limited preview. If you would like to opt-in to try it, contact support.
</Admonition>
## Manage MFA enforcement
+110 -31
View File
@@ -35,33 +35,122 @@ To use PrivateLink with your Supabase project:
- AWS VPC in the same region as your Supabase project
- Appropriate permissions to accept Resource Shares, and create and manage endpoints
<Admonition type="caution">
PrivateLink connections are region-specific. Your VPC and Supabase project must be in the same AWS region to establish the connection.
</Admonition>
## Getting started
PrivateLink setup requires coordination between your team and Supabase. The process involves sharing your AWS Account ID(s) and accepting a Resource Share.
#### Step 1: Contact Supabase support
### Setup process
Reach out to your Enterprise account manager or [contact our team](https://supabase.com/contact/enterprise) to initiate PrivateLink setup. During this initial contact, be prepared to provide:
1. **Contact Supabase Support**: Reach out to your Enterprise account manager or [contact our team](https://supabase.com/contact/enterprise) to initiate PrivateLink setup
2. **Provide AWS Account Details**: Share your AWS Account ID(s) with our team. Optionally specify which Supabase projects you want to enable (otherwise all projects in your organization will be included)
3. **Accept Resource Share**: Supabase will send you an AWS Resource Share containing the VPC Lattice Resource Configurations for your projects. Accept this share from your AWS console
4. **Create Connection**: In your AWS account, either [create a PrivateLink endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/privatelink-access-resources.html) or [attach the Resource Configuration](https://docs.aws.amazon.com/vpc-lattice/latest/ug/service-network-associations.html) to an existing VPC Lattice Service Network
5. **Test Connectivity**: Verify the private connection is working correctly from your VPC
6. **Update Applications**: Configure your applications to use the private connection details
7. **Disable Public Connectivity**: Optionally, disable public internet access for your database to enforce private-only connectivity
- Your Supabase organization slug
- The specific projects you want to enable PrivateLink for (optional)
- Your AWS Account ID(s)
### DNS and connectivity
#### Step 2: Accept resource share
Once PrivateLink is configured:
Supabase will send you an AWS Resource Share containing the VPC Lattice Resource Configurations for your projects. To accept this share:
- You may configure a custom DNS record to point to your PrivateLink endpoint interface or the endpoints on.aws DNS record within your VPC
- Applications will need to be updated to use the PrivateLink endpoint
- Standard database monitoring and observability tools will continue to work through the private connection
1. Login to your AWS Management Console, ensure you are in the AWS region where your Supabase project is located
2. Navigate to the AWS Resource Access Manager (RAM) console
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
3. Go to [Shared with me > Resource shares](https://console.aws.amazon.com/ram/home#SharedResourceShares)
4. Locate the resource share from Supabase.
- The resource share will have the format `cust-prod-[region]-pl-[organisation]-rc-share`
5. Click on the resource share name to view details. Review the list of resource shares - it should only include resources of type vpc-lattice:ResourceConfiguration.
6. Click **Accept resource share**
7. Confirm the acceptance in the dialog box
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
After accepting, you'll see the resource configurations appear in your [Shared with me > Shared resources](https://console.aws.amazon.com/ram/home#SharedResources) section of the RAM console and the [PrivateLink and Lattice > Resource configurations](https://console.aws.amazon.com/vpcconsole/home#ResourceConfigs) section of the VPC console.
#### Step 3: Configure security groups
Ensure your security groups allow traffic on the appropriate ports:
1. Navigate to the [VPC console > Security Groups](https://console.aws.amazon.com/vpcconsole/home#SecurityGroups:)
2. Create a new security group for the endpoint or service network by clicking [Create security group](https://console.aws.amazon.com/vpcconsole/home#CreateSecurityGroup:)
3. Give your security group a descriptive name and select the appropriate VPC
4. Add an inbound rule for:
- Type: Postgres (TCP, port 5432)
- Destination that is appropriate for your network. i.e. the subnet of your VPC or security group of your application instances
5. Finish creating the security group by clicking **Create security group**
#### Step 4: Create connection
In your AWS account, you have two options to establish connectivity:
##### Option A: Create a PrivateLink endpoint
1. Navigate to the VPC console in your AWS account
2. Go to [Endpoints](https://console.aws.amazon.com/vpcconsole/home#Endpoints:) in the left sidebar
3. Click [Create endpoint](https://console.aws.amazon.com/vpcconsole/home#CreateVpcEndpoint:)
4. Give your endpoint a name (e.g. `supabase-privatelink-[project name]`)
5. Under Type, select **Resources**
6. In the **Resource configurations** section select the appropriate resource configuration
- The resource configuration name will be in the format `[organisation]-[project-ref]-rc`
7. Select your VPC from the dropdown. This should match the VPC you selected for your security group in Step 3
8. Enable the **Enable DNS name** option if you want to use a DNS record instead of the endpoints IP address(es)
9. Choose the appropriate subnets for your network
- AWS will provision a private ENI for you in each selected subnet
- IP address type should be set to IPv4
10. Choose the security group you created in Step 3.
11. Click **Create endpoint**
12. After creation, you will see the endpoint in the [Endpoints](https://console.aws.amazon.com/vpcconsole/home#Endpoints:) section with a status of "Available"
13. For connectivity:
- The IP addresses of the endpoint will be listed in the **Subnets** section of the endpoint details
- The DNS record will be in the **Associations** section of the endpoint details in the **DNS Name** field if you enabled it in step 8
##### Option B: Attach resource configuration to an existing VPC lattice service network
1. **This method is only recommended if you have an existing VPC Lattice Service Network**
2. Navigate to the VPC Lattice console in your AWS account
3. Go to [Service networks](https://console.aws.amazon.com/vpcconsole/home#ServiceNetworks) in the left sidebar and select your service network
4. In the service network details, go to the **Resource configuration associations** tab
5. Click **Create associations**
6. Select the appropriate **Resource configuration** from the dropdown
7. Click **Save changes**
8. After creation, you will see the resource configuration in the Resource configurations section of your service network with the status "Active"
9. For connectivity, click on the association details and the domain name will be listed in the **DNS entries** section
#### Step 5: Test connectivity
Verify the private connection is working correctly from your VPC:
1. Launch an EC2 instance or use an existing instance in your VPC
2. Install a Postgres client (e.g., `psql`)
3. Test the connection using the private endpoint:
```bash
psql "postgresql://[username]:[password]@[private-endpoint]:5432/postgres"
```
You should see a successful connection without any public internet traffic.
#### Step 6: Update applications
Configure your applications to use the private connection details:
1. Update your database connection strings to use the private endpoint hostname
2. Ensure your application instances are in the same VPC or connected VPCs
3. Update any database connection pooling configurations
4. Test application connectivity thoroughly
Example connection string update:
```
# Before (public)
postgresql://user:pass@db.[project-ref].supabase.co:5432/postgres
# After (private)
postgresql://user:pass@your-private-endpoint.vpce.amazonaws.com:5432/postgres
```
#### Step 8: Disable public connectivity (optional)
For maximum security, you can disable public internet access for your database:
1. Contact Supabase support to disable public connectivity
2. Ensure all applications are successfully using the private connection
3. Update any monitoring or backup tools to use the private endpoint
## Alpha limitations
@@ -72,20 +161,10 @@ During the alpha phase:
## Compatibility
The PrivateLink endpoint behaves like a standard Postgres endpoint, allowing you to connect using:
The PrivateLink endpoint is a layer 3 solution so behaves like a standard Postgres endpoint, allowing you to connect using:
- Direct Postgres connections using standard tools
- Third-party database tools and ORMs (with the appropriate routing)
- (PgBouncer Projects Only) Directly to the pooler.
## Use cases
PrivateLink is ideal for organizations requiring:
- **Regulatory Compliance**: Meeting strict data governance requirements that mandate private network connectivity
- **Enhanced Security**: Eliminating public internet exposure for sensitive database workloads
- **Corporate Policies**: Adhering to enterprise security policies that prohibit database connections over public networks
- **Performance Optimization**: Benefiting from reduced latency through AWS's private network infrastructure
## Next steps
@@ -18,6 +18,12 @@ Realtime Authorization is in Public Beta. To use Authorization for your Realtime
</Admonition>
<Admonition type="note">
To enforce private channels you need to disable the 'Allow public access' setting in [Realtime Settings](https://supabase.com/dashboard/project/_?featurePreviewModal=supabase-ui-realtime-settings)
</Admonition>
## How it works
Realtime uses the `messages` table in your database's `realtime` schema to generate access policies for your clients when they connect to a Channel topic.
@@ -26,6 +26,18 @@ const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>'
const roomOne = supabase.channel('room-one') // set your topic here
```
## Authorization
Authorization is done via RLS policies against the table `realtime.messages` which will determine if a user can connect to a Channel and if they are allowed to send messages to a Channel.
By default, channels are public and you need to set that you want to use a private channel.
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://<project>.supabase.co', '<your-anon-key>')
const roomOne = supabase.channel('private-room-one', { config: { private: true } })
```
## Broadcast
Realtime Broadcast follows the [publish-subscribe pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) where a client publishes messages to a channel based on a unique topic. For example, a user could send a message to a channel with topic `room-one`.
@@ -45,6 +57,8 @@ roomOne.send({
Other clients can receive the message in real-time by subscribing to the Channel with topic `room-one`. These clients continue to receive messages as long as they are subscribed and connected to the same Channel topic.
You can also use [Broadcast using the REST API](/docs/guides/realtime/broad cast#broadcast-using-the-rest-api) and [Broadcast using the Database](/docs/guides/realtime/broadcast#broadcast-using-the-database) to send messages to a Channel which allows you to do more advanced use-cases.
An example use-case is sharing a user's cursor position with other clients in an online game.
## Presence
@@ -93,6 +107,36 @@ Anyone with access to a valid JWT signed with the project's JWT secret is able t
Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the `public` schema and you must first enable the tables you want your clients to listen to.
## Settings
<Admonition type="note">
Realtime settings are currently under Feature Preview section in the dashboard.
</Admonition>
<Image
alt="Usage page navigation bar"
src={{
light: '/docs/img/guides/platform/realtime-settings--light.png',
dark: '/docs/img/guides/platform/realtime-settings--dark.png',
}}
zoomable
/>
You can set the following settings using the Realtime Settings screen in your Dashboard:
- Channel Restrictions: You can toggle this settings to set Realtime to allow public channels or set it to use only private channels with [Realtime Authorization](/docs/content/guides/realtime/authorization).
- Database connection pool size: Determines the number of connections used for Realtime Authorization RLS checking
{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
- Max concurrent clients: Determines the maximum number of clients that can be connected
All changes made in this screen will disconnect all your connected clients to ensure Realtime starts with the appropriate settings and all changes are stored in Supabase middleware.
## Choosing between Broadcast and Postgres Changes for database changes
We recommend using Broadcast by default and using Broadcast from Database specifically as it will allow you to scale your application compared to Postgres Changes.
## Choosing between Broadcast and Presence
We recommend using Broadcast by default, and then Presence when required. Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast. This is computationally heavy, so you should use it sparingly. If you use Presence, it's best to throttle your changes so that you are sending updates less frequently.
+6
View File
@@ -17,6 +17,12 @@ The [HIPAA Compliance Guide](/docs/guides/security/hipaa-compliance) explains Su
# Platform configuration
As a hosted platform, Supabase provides additional security controls to further enhance the security posture depending on organizations' own requirements or obligations.
These can be found under the [dedicated security page](https://supabase.com/dashboard/org/_/security) under organization settings. And are described in greater detail [here](/docs/guides/security/platform-security).
# Product configuration
Each product offered by Supabase comes with customizable security controls and these security controls help ensure that applications built on Supabase are secure, compliant, and resilient against various threats.
The [security configuration guides](/docs/guides/security/product-security) provide detailed information for configuring individual products.
@@ -0,0 +1,66 @@
---
id: 'platform-security'
title: 'Secure configuration of Supabase platform'
description: 'Supabase provides a secure yet flexible platform. Here is how to adjust various security settings across the platform.'
---
The Supabase hosted platform provides a secure by default configuration. Some organizations may however require further security controls to meet their own security policies or compliance requirements.
Access to additional security controls can be found under the [security tab](https://supabase.com/dashboard/org/_/security) for organizations.
## Available controls
<Admonition type="note">
Additional security controls are under active development. Any changes will be published here and
in our [changelog](https://supabase.com/changelog).
</Admonition>
### Enforce multi-factor authentication (MFA)
Organization owners can choose to enforce MFA for all team members.
For configuration information, see [Enforce MFA on Organization](/docs/guides/platform/mfa/org-mfa-enforcement)
### SSO for organizations
Supabase offers single sign-on (SSO) as a login option to provide additional account security for your team. This allows company administrators to enforce the use of an identity provider when logging into Supabase.
For configuration information, see [Enable SSO for Your Organization](/docs/guides/platform/sso).
### Postgres SSL enforcement
Supabase projects support connecting to the Postgres DB without SSL enforced to maximize client compatibility. For increased security, you can prevent clients from connecting if they're not using SSL.
For configuration information, see [Postgres SSL Enforcement](/docs/guides/platform/ssl-enforcement)
<Admonition type="note">
Controlling this at the organization level is on our roadmap.
</Admonition>
### Network restrictions
Each Supabase project comes with configurable restrictions on the IP ranges that are allowed to connect to Postgres and its pooler ("your database"). These restrictions are enforced before traffic reaches the database. If a connection is not restricted by IP, it still needs to authenticate successfully with valid database credentials.
For configuration information, see [Network Restrictions](/docs/guides/platform/network-restrictions)
<Admonition type="note">
Controlling this at the organization level is on our roadmap.
</Admonition>
### PrivateLink
PrivateLink provides enterprise-grade private network connectivity between your AWS VPC and your Supabase database using AWS VPC Lattice. This eliminates exposure to the public internet by creating a secure, private connection that keeps your database traffic within the AWS network backbone.
For configuration information, see [PrivateLink](/docs/guides/platform/privatelink)
<Admonition type="note">
PrivateLink is currently in alpha and available exclusively to Enterprise customers.
</Admonition>
@@ -1,7 +1,7 @@
---
id: 'product-security'
title: 'Secure configuration of Supabase products'
description: 'Supabase provides a secure yet flexible platform. Here is how to adjust various security settings across the platform.'
description: 'Supabase provides a secure yet flexible set of products. Here is how to adjust various security settings across the various products.'
---
The Supabase [production checklist](/docs/guides/deployment/going-into-prod) provides detailed advice on preparing an app for production. While our [SOC 2](/docs/guides/security/soc-2-compliance) and [HIPAA](/docs/guides/security/hipaa-compliance) compliance documents outline the roles and responsibilities for building a secure and compliant app.
@@ -0,0 +1,187 @@
---
title: 'Connecting to Analytics Buckets'
---
<Admonition type="caution">
This feature is in **Private Alpha**. API stability and backward compatibility are not guaranteed at this stage. Reach out from this [Form](https://forms.supabase.com/analytics-buckets) to request access
</Admonition>
When interacting with Analytics Buckets, you authenticate against two main services - the Iceberg REST Catalog and the S3-Compatible Storage Endpoint.
The **Iceberg REST Catalog** acts as the central management system for Iceberg tables. It allows Iceberg clients, such as PyIceberg and Apache Spark, to perform metadata operations including:
- Creating and managing tables and namespaces
- Tracking schemas and handling schema evolution
- Managing partitions and snapshots
- Ensuring transactional consistency and isolation
The REST Catalog itself does not store the actual data. Instead, it stores metadata describing the structure, schema, and partitioning strategy of Iceberg tables.
Actual data storage and retrieval operations occur through the separate S3-compatible endpoint, optimized for reading and writing large analytical datasets stored in Parquet files.
## Authentication
To connect to an Analytics Bucket, you will need
- An Iceberg client (Spark, PyIceberg, etc) which supports the REST Catalog interface.
- S3 credentials to authenticate your Iceberg client with the underlying S3 Bucket.
To create S3 Credentials go to [**Project Settings > Storage**](https://supabase.com/dashboard/project/_/settings/storage), for more information, see the [S3 Authentication Guide](https://supabase.com/docs/guides/storage/s3/authentication). We will support other authentication methods in the future.
- The project reference and Service key for your Supabase project.
You can find your Service key in the Supabase Dashboard under [**Project Settings > API**.](https://supabase.com/dashboard/project/_/settings/api-keys)
You will now have an **Access Key** and a **Secret Key** that you can use to authenticate your Iceberg client.
## Connecting via PyIceberg
PyIceberg is a Python client for Apache Iceberg, facilitating interaction with Iceberg Buckets.
**Installation**
```bash
pip install pyiceberg pyarrow
```
Here's a comprehensive example using PyIceberg with clearly separated configuration:
```python
from pyiceberg.catalog import load_catalog
import pyarrow as pa
import datetime
# Supabase project ref
PROJECT_REF = "<your-supabase-project-ref>"
# Configuration for Iceberg REST Catalog
WAREHOUSE = "your-analytics-bucket-name"
TOKEN = "SERVICE_KEY"
# Configuration for S3-Compatible Storage
S3_ACCESS_KEY = "KEY"
S3_SECRET_KEY = "SECRET"
S3_REGION = "PROJECT_REGION"
S3_ENDPOINT = f"https://{PROJECT_REF}.supabase.co/storage/v1/s3"
CATALOG_URI = f"https://{PROJECT_REF}.supabase.co/storage/v1/iceberg"
# Load the Iceberg catalog
catalog = load_catalog(
"analytics-bucket",
type="rest",
warehouse=WAREHOUSE,
uri=CATALOG_URI,
token=TOKEN,
**{
"py-io-impl": "pyiceberg.io.pyarrow.PyArrowFileIO",
"s3.endpoint": S3_ENDPOINT,
"s3.access-key-id": S3_ACCESS_KEY,
"s3.secret-access-key": S3_SECRET_KEY,
"s3.region": S3_REGION,
"s3.force-virtual-addressing": False,
},
)
# Create namespace if it doesn't exist
catalog.create_namespace_if_not_exists("default")
# Define schema for your Iceberg table
schema = pa.schema([
pa.field("event_id", pa.int64()),
pa.field("event_name", pa.string()),
pa.field("event_timestamp", pa.timestamp("ms")),
])
# Create table (if it doesn't exist already)
table = catalog.create_table_if_not_exists(("default", "events"), schema=schema)
# Generate and insert sample data
current_time = datetime.datetime.now()
data = pa.table({
"event_id": [1, 2, 3],
"event_name": ["login", "logout", "purchase"],
"event_timestamp": [current_time, current_time, current_time],
})
# Append data to the Iceberg table
table.append(data)
# Scan table and print data as pandas DataFrame
df = table.scan().to_pandas()
print(df)
```
## Connecting via Apache Spark
Apache Spark allows distributed analytical queries against Iceberg Buckets.
```python
from pyspark.sql import SparkSession
# Supabase project ref
PROJECT_REF = "<your-supabase-ref>"
# Configuration for Iceberg REST Catalog
WAREHOUSE = "your-analytics-bucket-name"
TOKEN = "SERVICE_KEY"
# Configuration for S3-Compatible Storage
S3_ACCESS_KEY = "KEY"
S3_SECRET_KEY = "SECRET"
S3_REGION = "PROJECT_REGION"
S3_ENDPOINT = f"https://{PROJECT_REF}.supabase.co/storage/v1/s3"
CATALOG_URI = f"https://{PROJECT_REF}.supabase.co/storage/v1/iceberg"
# Initialize Spark session with Iceberg configuration
spark = SparkSession.builder \
.master("local[*]") \
.appName("SupabaseIceberg") \
.config("spark.driver.host", "127.0.0.1") \
.config("spark.driver.bindAddress", "127.0.0.1") \
.config('spark.jars.packages', 'org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.6.1,org.apache.iceberg:iceberg-aws-bundle:1.6.1') \
.config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") \
.config("spark.sql.catalog.my_catalog", "org.apache.iceberg.spark.SparkCatalog") \
.config("spark.sql.catalog.my_catalog.type", "rest") \
.config("spark.sql.catalog.my_catalog.uri", CATALOG_URI) \
.config("spark.sql.catalog.my_catalog.warehouse", WAREHOUSE) \
.config("spark.sql.catalog.my_catalog.token", TOKEN) \
.config("spark.sql.catalog.my_catalog.s3.endpoint", S3_ENDPOINT) \
.config("spark.sql.catalog.my_catalog.s3.path-style-access", "true") \
.config("spark.sql.catalog.my_catalog.s3.access-key-id", S3_ACCESS_KEY) \
.config("spark.sql.catalog.my_catalog.s3.secret-access-key", S3_SECRET_KEY) \
.config("spark.sql.catalog.my_catalog.s3.remote-signing-enabled", "false") \
.config("spark.sql.defaultCatalog", "my_catalog") \
.getOrCreate()
# SQL Operations
spark.sql("CREATE NAMESPACE IF NOT EXISTS analytics")
spark.sql("""
CREATE TABLE IF NOT EXISTS analytics.users (
user_id BIGINT,
username STRING
)
USING iceberg
""")
spark.sql("""
INSERT INTO analytics.users (user_id, username)
VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie')
""")
result_df = spark.sql("SELECT * FROM analytics.users")
result_df.show()
```
## Connecting to the Iceberg REST Catalog directly
To authenticate with the Iceberg REST Catalog directly, you need to provide a valid Supabase **Service key** as a Bearer token.
```
curl \
--request GET -sL \
--url 'https://<your-supabase-project>.supabase.co/storage/v1/iceberg/v1/config?warehouse=<bucket-name>' \
--header 'Authorization: Bearer <your-service-key>'
```
@@ -0,0 +1,38 @@
---
title: 'Creating Analytics Buckets'
subtitle: ''
---
<Admonition type="caution">
This feature is in **Private Alpha**. API stability and backward compatibility are not guaranteed at this stage. Reach out from this [Form](https://forms.supabase.com/analytics-buckets) to request access
</Admonition>
Analytics Buckets use [Apache Iceberg](https://iceberg.apache.org/), an open-table format for managing large analytical datasets.
You can interact with them using tools such as [PyIceberg](https://py.iceberg.apache.org/), [Apache Spark](https://spark.apache.org/) or any client which supports the [standard Iceberg REST Catalog API](https://editor-next.swagger.io/?url=https://raw.githubusercontent.com/apache/iceberg/main/open-api/rest-catalog-open-api.yaml).
You can create an Analytics Bucket using either the Supabase SDK or the Supabase Dashboard.
### Using the Supabase SDK
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project.supabase.co', 'your-service-key')
supabase.storage.createBucket('my-analytics-bucket', {
type: 'ANALYTICS',
})
```
### Using the Supabase Dashboard
1. Navigate to the Storage section in the Supabase Dashboard.
2. Click on "Create Bucket".
3. Enter a name for your bucket (e.g., my-analytics-bucket).
4. Select "Analytics Bucket" as the bucket type.
<img alt="Storage schema design" src="/docs/img/storage/iceberg-bucket.png" />
Now, that you have created your Analytics Bucket, you can start [connecting to it](/docs/guides/storage/analytics/connecting-to-analytics-bucket) with Iceberg clients like PyIceberg or Apache Spark.
@@ -0,0 +1,24 @@
---
title: 'Analytics Buckets'
subtitle: ''
---
<Admonition type="caution">
This feature is in **Private Alpha**. API stability and backward compatibility are not guaranteed at this stage. Reach out from this [Form](https://forms.supabase.com/analytics-buckets) to request access
</Admonition>
**Analytics Buckets** are designed for analytical workflows on large datasets without impacting your main database.
Postgres tables are optimized for handling real-time, transactional workloads with frequent inserts, updates, deletes and low-latency queries. **Analytical workloads** have very different requirements: processing large volumes of historical data, running complex queries and aggregations, minimizing storage costs, and ensuring these analytical queries do not interfere with the production traffic.
**Analytics Buckets** address these requirements using [Apache Iceberg](https://iceberg.apache.org/), an open-table format for managing large analytical datasets efficiently.
Analytics Buckets are ideal for
• Data warehousing and business intelligence
• Historical data archiving
• Periodically refreshed real-time analytics
• Complex analytical queries over large datasets
By separating transactional and analytical workloads, Supabase makes it easy to build scalable analytics pipelines without impacting your primary Postgres performance.
@@ -0,0 +1,23 @@
---
title: 'Analytics Buckets Limits'
subtitle: ''
---
<Admonition type="caution">
This feature is in **Private Alpha**. API stability and backward compatibility are not guaranteed at this stage. Reach out from this [Form](https://forms.supabase.com/analytics-buckets) to request access
</Admonition>
The following default limits are applied when this feature is in the private alpha stage, they can be adjusted on a case-by-case basis:
| **Category** | **Limit** |
| --------------------------------------- | --------- |
| Number of Analytics Buckets per project | 2 |
| Number of namespaces per bucket | 10 |
| Number of tables per namespace | 10 |
## Pricing
Analytics Buckets are Free to use during the Private Alpha phase,
however, you'll still be charged for the underlying egress.
@@ -0,0 +1,309 @@
---
id: 'reports'
title: 'Reports'
description: 'Built-in observability for your Supabase project'
---
Supabase Reports provide comprehensive observability for your project through dedicated monitoring dashboards that visualize key metrics across your database, auth, storage, realtime, and API systems. Each report offers self-debugging tools to gain actionable insights for optimizing performance and troubleshooting issues.
<Admonition type="note">
Reports are only available for projects hosted on the Supabase Cloud platform and are not available for self-hosted instances.
</Admonition>
## Using reports
Reports can be filtered by time range to focus your analysis on specific periods. Available time ranges are gated by your organization's plan, with higher-tier plans providing access to longer historical periods.
| Time Range | Free | Pro | Team | Enterprise |
| --------------- | ---- | --- | ---- | ---------- |
| Last 10 minutes | ✅ | ✅ | ✅ | ✅ |
| Last 30 minutes | ✅ | ✅ | ✅ | ✅ |
| Last 60 minutes | ✅ | ✅ | ✅ | ✅ |
| Last 3 hours | ✅ | ✅ | ✅ | ✅ |
| Last 24 hours | ✅ | ✅ | ✅ | ✅ |
| Last 7 days | ❌ | ✅ | ✅ | ✅ |
| Last 14 days | ❌ | ❌ | ✅ | ✅ |
| Last 28 days | ❌ | ❌ | ✅ | ✅ |
---
## Database
The Database report provides the most comprehensive view into your Postgres instance's health and performance characteristics. These charts help you identify performance bottlenecks, resource constraints, and optimization opportunities at a glance.
The following charts are available for Free and Pro plans:
| Chart | Available Plans | Description | Key Insights |
| ---------------------------- | --------------- | -------------------------------------------- | --------------------------------------------- |
| Memory usage | Free, Pro | RAM usage percentage by the database | Memory pressure and resource utilization |
| CPU usage | Free, Pro | Average CPU usage percentage | CPU-intensive query identification |
| Disk IOPS | Free, Pro | Read/write operations per second with limits | IO bottleneck detection and workload analysis |
| Database connections | Free, Pro | Number of pooler connections to the database | Connection pool monitoring |
| Shared Pooler connections | All | Client connections to the shared pooler | Shared pooler usage patterns |
| Dedicated Pooler connections | All | Client connections to PgBouncer | Dedicated pooler connection monitoring |
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
### Advanced Telemetry
The following charts provide a more advanced and detailed view of your database performance and are available only for Teams and Enterprise plans.
### Memory usage
<Image
alt="Memory usage chart"
zoomable
src={{
dark: '/docs/img/database/reports/memory-usage-chart-dark.png',
light: '/docs/img/database/reports/memory-usage-chart-light.png',
}}
/>
| Component | Description |
| ------------------- | ------------------------------------------------------ |
| **Used** | RAM actively used by Postgres and the operating system |
| **Cache + buffers** | Memory used for page cache and Postgres buffers |
| **Free** | Available unallocated memory |
How it helps debug issues:
| Issue | Description |
| ------------------------------ | ------------------------------------------------ |
| Memory pressure detection | Identify when free memory is consistently low |
| Cache effectiveness monitoring | Monitor cache performance for query optimization |
| Memory leak detection | Detect inefficient memory usage patterns |
Actions you can take:
| Action | Description |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [Upgrade compute size](https://supabase.com/docs/guides/platform/compute-and-disk#compute-size) | Increase available memory resources |
| Optimize queries | Reduce memory consumption of expensive queries |
| Tune Postgres configuration | Improve memory management settings |
| Implement application caching | Add query result caching to reduce memory load |
### CPU usage
<Image
alt="CPU usage chart"
zoomable
src={{
dark: '/docs/img/database/reports/cpu-usage-chart-dark.png',
light: '/docs/img/database/reports/cpu-usage-chart-light.png',
}}
/>
| Category | Description |
| ---------- | ------------------------------------------------ |
| **System** | CPU time for kernel operations |
| **User** | CPU time for database queries and user processes |
| **IOWait** | CPU time waiting for disk/network IO |
| **IRQs** | CPU time handling interrupts |
| **Other** | CPU time for miscellaneous tasks |
How it helps debug issues:
| Issue | Description |
| ---------------------------------- | -------------------------------------------------- |
| CPU-intensive query identification | Identify expensive queries when User CPU is high |
| IO bottleneck detection | Detect disk/network issues when IOWait is elevated |
| System overhead monitoring | Monitor resource contention and kernel overhead |
Actions you can take:
| Action | Description |
| -------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Optimize CPU-intensive queries | Target queries causing high User CPU usage |
| Address IO bottlenecks | Resolve disk/network issues when IOWait is high |
| [Upgrade compute size](/docs/guides/platform/compute-and-disk) | Increase available CPU capacity |
| Implement proper indexing | Use [query optimization](/docs/guides/database/postgres/indexes) techniques |
### Disk input/output operations per second (IOPS)
<Image
alt="Disk IOPS chart"
zoomable
src={{
dark: '/docs/img/database/reports/disk-iops-chart-dark.png',
light: '/docs/img/database/reports/disk-iops-chart-light.png',
}}
/>
This chart displays read and write IOPS with a reference line showing your compute size's maximum IOPS capacity.
How it helps debug issues:
| Issue | Description |
| --------------------------------- | ---------------------------------------------------------------- |
| Disk IO bottleneck identification | Identify when disk IO becomes a performance constraint |
| Workload pattern analysis | Distinguish between read-heavy vs write-heavy operations |
| Performance correlation | Spot disk activity spikes that correlate with performance issues |
Actions you can take:
| Action | Description |
| ---------------------------------------------------------------------------------- | --------------------------------------------------------- |
| Optimize indexing | Reduce high read IOPS through better query indexing |
| Consider read replicas | Distribute read-heavy workloads across multiple instances |
| Batch write operations | Reduce write IOPS by grouping database writes |
| [Upgrade compute size](https://supabase.com/docs/guides/platform/compute-and-disk) | Increase IOPS limits with larger compute instances |
### Disk IO Usage
<Image
alt="Disk IO Usage chart"
zoomable
src={{
dark: '/docs/img/database/reports/disk-io-chart-dark.png',
light: '/docs/img/database/reports/disk-io-chart-light.png',
}}
/>
This chart displays the percentage of your allocated IOPS (Input/Output Operations Per Second) currently being used.
How it helps debug issues:
| Issue | Description |
| --------------------------- | ----------------------------------------------------------- |
| IOPS limit monitoring | Identify when approaching your allocated IOPS capacity |
| Performance correlation | Correlate high IO usage with application performance issues |
| Operation impact assessment | Monitor how database operations affect disk performance |
Actions you can take:
| Action | Description |
| ---------------------------------------------------------------------------------- | -------------------------------------------------- |
| Optimize disk-intensive queries | Reduce queries that perform excessive reads/writes |
| Add strategic indexes | Reduce sequential scans with appropriate indexing |
| [Upgrade compute size](https://supabase.com/docs/guides/platform/compute-and-disk) | Increase IOPS limits with larger compute instances |
| Review database design | Optimize schema and query patterns for efficiency |
### Disk size
<Image
alt="Disk Size chart"
zoomable
src={{
dark: '/docs/img/database/reports/disk-size-chart-dark.png',
light: '/docs/img/database/reports/disk-size-chart-light.png',
}}
/>
| Component | Description |
| ------------ | --------------------------------------------------------- |
| **Database** | Space used by your actual database data (tables, indexes) |
| **WAL** | Space used by Write-Ahead Logging |
| **System** | Reserved space for system operations |
How it helps debug issues:
| Issue | Description |
| ----------------------------- | ------------------------------------------- |
| Space consumption monitoring | Track disk usage trends over time |
| Growth pattern identification | Identify rapid growth requiring attention |
| Capacity planning | Plan upgrades before hitting storage limits |
Actions you can take:
| Action | Description |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Run [VACUUM](https://www.postgresql.org/docs/current/sql-vacuum.html) operations | Reclaim dead tuple space and optimize storage |
| Analyze large tables | Use CLI commands like `table-sizes` to identify optimization targets |
| Implement data archival | Archive historical data to reduce active storage needs |
| [Upgrade disk size](https://supabase.com/docs/guides/platform/database-size) | Increase storage capacity when approaching limits |
### Database connections
<Image
alt="Database connections chart"
zoomable
src={{
dark: '/docs/img/database/reports/db-connections-chart-dark.png',
light: '/docs/img/database/reports/db-connections-chart-light.png',
}}
/>
| Connection Type | Description |
| --------------- | ------------------------------------------------ |
| **Postgres** | Direct connections from your application |
| **PostgREST** | Connections from the PostgREST API layer |
| **Reserved** | Administrative connections for Supabase services |
| **Auth** | Connections from Supabase Auth service |
| **Storage** | Connections from Supabase Storage service |
| **Other roles** | Miscellaneous database connections |
How it helps debug issues:
| Issue | Description |
| ------------------------------- | ----------------------------------------------------------- |
| Connection pool exhaustion | Identify when approaching maximum connection limits |
| Connection leak detection | Spot applications not properly closing connections |
| Service distribution monitoring | Monitor connection usage across different Supabase services |
Actions you can take:
| Action | Description |
| -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| [Upgrade compute size](https://supabase.com/docs/guides/platform/compute-and-disk#compute-size) | Increase maximum connection limits |
| Implement [connection pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#shared-pooler) | Optimize connection management for high direct connection usage |
| Review application code | Ensure proper connection handling and cleanup |
## Auth
The Auth report focuses on user authentication patterns and behaviors within your Supabase project.
| Chart | Description | Key Insights |
| ------------------------ | --------------------------------------------- | ----------------------------------------------- |
| Active Users | Count of unique users performing auth actions | User engagement and retention patterns |
| Sign In Attempts by Type | Breakdown of authentication methods used | Password vs OAuth vs magic link preferences |
| Sign Ups | Total new user registrations | Growth trends and onboarding funnel performance |
| Auth Errors | Error rates grouped by status code | Authentication friction and security issues |
| Password Reset Requests | Volume of password recovery attempts | User experience pain points |
## Storage
The Storage report provides visibility into how your Supabase Storage is being utilized, including request patterns, performance characteristics, and caching effectiveness.
| Chart | Description | Key Insights |
| --------------- | ------------------------------------------ | ------------------------------------------------------ |
| Total Requests | Overall request volume to Storage | Traffic patterns and usage trends |
| Response Speed | Average response time for storage requests | Performance bottlenecks and optimization opportunities |
| Network Traffic | Ingress and egress bandwidth usage | Data transfer costs and CDN effectiveness |
| Request Caching | Cache hit rates and miss patterns | CDN performance and cost optimization |
| Top Routes | Most frequently accessed storage paths | Popular content and usage patterns |
## Realtime
The Realtime report tracks WebSocket connections, channel activity, and real-time event patterns in your Supabase project.
| Chart | Description | Key Insights |
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------- |
| Realtime Connections | Active WebSocket connections over time | Concurrent user activity and connection stability |
| Channel Events | Breakdown of broadcast, Postgres changes, and presence events | Real-time feature usage patterns |
| Rate of Channel Joins | Frequency of new channel subscriptions | User engagement with real-time features |
| Total Requests | HTTP requests to Realtime endpoints | API usage alongside WebSocket activity |
| Response Speed | Performance of Realtime API endpoints | Infrastructure optimization opportunities |
## Edge Functions
The Edge Functions report provides insights into serverless function performance, execution patterns, and regional distribution across Supabase's global edge network.
| Chart | Description | Key Insights |
| ---------------------- | ----------------------------------------- | ---------------------------------------------- |
| Execution Status Codes | Function response codes and error rates | Function reliability and error patterns |
| Execution Time | Average function duration and performance | Performance optimization opportunities |
| Invocations by Region | Geographic distribution of function calls | Global usage patterns and latency optimization |
## API gateway
The API Gateway report analyzes traffic patterns and performance characteristics of requests flowing through your Supabase project's API layer.
| Chart | Description | Key Insights |
| --------------- | ----------------------------------------- | ------------------------------------------------ |
| Total Requests | Overall API request volume | Traffic patterns and growth trends |
| Response Errors | Error rates with 4XX and 5XX status codes | API reliability and user experience issues |
| Response Speed | Average API response times | Performance bottlenecks and optimization targets |
| Network Traffic | Request and response bandwidth usage | Data transfer patterns and cost implications |
| Top Routes | Most frequently accessed API endpoints | Usage patterns and optimization priorities |
+7 -7
View File
@@ -151,25 +151,25 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab
<TabPanel id="kotlin" label="build.gradle.kts">
```kotlin
val commonMain by getting {
commonMain {
dependencies {
//supabase modules
//Supabase modules
}
}
val jvmMain by getting {
jvmMain {
dependencies {
implementation("io.ktor:ktor-client-cio:KTOR_VERSION")
}
}
val androidMain by getting {
dependsOn(jvmMain)
androidMain {
dependsOn(jvmMain.get())
}
val jsMain by getting {
jsMain {
dependencies {
implementation("io.ktor:ktor-client-js:KTOR_VERSION")
}
}
val iosMain by getting {
iosMain {
dependencies {
implementation("io.ktor:ktor-client-darwin:KTOR_VERSION")
}
File diff suppressed because it is too large. Load diff
+24 -1
View File
@@ -7,7 +7,10 @@ type Database = {
public: {
Tables: DatabaseGenerated['public']['Tables']
Views: DatabaseGenerated['public']['Views']
Functions: Omit<DatabaseGenerated['public']['Functions'], 'search_content'> & {
Functions: Omit<
DatabaseGenerated['public']['Functions'],
'search_content' | 'search_content_hybrid'
> & {
search_content: {
Args: Omit<
DatabaseGenerated['public']['Functions']['search_content']['Args'],
@@ -28,6 +31,26 @@ type Database = {
}
>
}
search_content_hybrid: {
Args: Omit<
DatabaseGenerated['public']['Functions']['search_content_hybrid']['Args'],
'query_embedding'
> & { query_embedding: Array<number> }
Returns: Array<
Omit<
DatabaseGenerated['public']['Functions']['search_content_hybrid']['Returns'][number],
'subsections' | 'metadata'
> & {
metadata: {
subtitle?: string
language?: string
methodName?: string
platform?: string
}
subsections: Array<{ title?: string; href?: string; content?: string }>
}
>
}
}
Enums: DatabaseGenerated['public']['Enums']
CompositeTypes: DatabaseGenerated['public']['CompositeTypes']
+1
View File
@@ -71,6 +71,7 @@ Laura C
Laurence Isla
Lenny Urbanowski
Long Hoang
Luca Forstner
Łukasz Niemier
Margarita Sandomirskaia
Mark Burggraf
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,9 @@
stateDiagram-v2
[*] --> standby : A new key is created and advertized
standby --> in_use : Once all components have picked up the new key, new JWTs can be issued with it
in_use --> previously_used : Rotation, JWT remain accepted
previously_used --> revoked : Once all JWTs created with the previous key expire (or sooner)
revoked --> [*] : Delete permanently after 7 days
revoked --> standby
previously_used --> standby
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg aria-roledescription="stateDiagram" role="graphics-document document" viewBox="0 0 336 766" height="766" class="statediagram" xmlns="http://www.w3.org/2000/svg" width="336" id="container"><style>#container{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#container .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#container .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#container .error-icon{fill:#552222;}#container .error-text{fill:#552222;stroke:#552222;}#container .edge-thickness-normal{stroke-width:1px;}#container .edge-thickness-thick{stroke-width:3.5px;}#container .edge-pattern-solid{stroke-dasharray:0;}#container .edge-thickness-invisible{stroke-width:0;fill:none;}#container .edge-pattern-dashed{stroke-dasharray:3;}#container .edge-pattern-dotted{stroke-dasharray:2;}#container .marker{fill:#333333;stroke:#333333;}#container .marker.cross{stroke:#333333;}#container svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#container p{margin:0;}#container defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#container g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#container g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#container g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#container g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#container g.stateGroup line{stroke:#333333;stroke-width:1;}#container .transition{stroke:#333333;stroke-width:1;fill:none;}#container .stateGroup .composit{fill:white;border-bottom:1px;}#container .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#container .state-note{stroke:#aaaa33;fill:#fff5ad;}#container .state-note text{fill:black;stroke:none;font-size:10px;}#container .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#container .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#container .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#container .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#container .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#container .edgeLabel .label text{fill:#333;}#container .label div .edgeLabel{color:#333;}#container .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#container .node circle.state-start{fill:#333333;stroke:#333333;}#container .node .fork-join{fill:#333333;stroke:#333333;}#container .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#container .end-state-inner{fill:white;stroke-width:1.5;}#container .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#container .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#container #statediagram-barbEnd{fill:#333333;}#container .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#container .cluster-label,#container .nodeLabel{color:#131300;}#container .statediagram-cluster rect.outer{rx:5px;ry:5px;}#container .statediagram-state .divider{stroke:#9370DB;}#container .statediagram-state .title-state{rx:5px;ry:5px;}#container .statediagram-cluster.statediagram-cluster .inner{fill:white;}#container .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#container .statediagram-cluster .inner{rx:0;ry:0;}#container .statediagram-state rect.basic{rx:5px;ry:5px;}#container .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#container .note-edge{stroke-dasharray:5;}#container .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#container .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#container .statediagram-note text{fill:black;}#container .statediagram-note .nodeLabel{color:black;}#container .statediagram .edgeLabel{color:red;}#container #dependencyStart,#container #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#container .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#container :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerUnits="userSpaceOnUse" markerHeight="14" markerWidth="20" refY="7" refX="19" id="container_stateDiagram-barbEnd"><path d="M 19,7 L9,13 L14,7 L9,1 Z"></path></marker></defs><g class="root"><g class="clusters"></g><g class="edgePaths"><path marker-end="url(#container_stateDiagram-barbEnd)" style="fill:none;" class="edge-thickness-normal edge-pattern-solid transition" id="edge0" d="M228,22L228,30.167C228,38.333,228,54.667,228,71C228,87.333,228,103.667,228,111.833L228,120"></path><path marker-end="url(#container_stateDiagram-barbEnd)" style="fill:none;" class="edge-thickness-normal edge-pattern-solid transition" id="edge1" d="M202.194,160L186.495,172.167C170.796,184.333Line truncated

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

@@ -40,6 +40,34 @@ export abstract class SearchResultModel {
return matchResult
})
}
static async searchHybrid(
args: RootQueryTypeSearchDocsArgs,
requestedFields: Array<string>
): Promise<Result<SearchResultModel[], ApiErrorGeneric>> {
const query = args.query.trim()
const includeFullContent = requestedFields.includes('content')
const embeddingResult = await openAI().createContentEmbedding(query)
return embeddingResult.flatMapAsync(async (embedding) => {
const matchResult = new Result(
await supabase().rpc('search_content_hybrid', {
query_text: query,
query_embedding: embedding,
include_full_content: includeFullContent,
max_result: args.limit ?? 30,
})
)
.map((matches) =>
matches
.map(createModelFromMatch)
.filter((item): item is SearchResultInterface => item !== null)
)
.mapError(convertPostgrestToApiError)
return matchResult
})
}
}
function createModelFromMatch({
@@ -53,7 +53,7 @@ async function resolveSearchImpl(
): Promise<Result<Array<SearchResultModel>, ApiErrorGeneric>> {
const fieldsInfo = graphQLFields(info)
const requestedFields = Object.keys(fieldsInfo.nodes ?? fieldsInfo.edges?.node ?? {})
return await SearchResultModel.search(args, requestedFields)
return await SearchResultModel.searchHybrid(args, requestedFields)
}
export const searchRoot = {
@@ -111,6 +111,6 @@ export class LintWarningsGuideSource extends BaseSource {
extractIndexedContent(): string {
const sections = this.sections ?? []
const sectionText = sections.map(({ content }) => content).join('\n\n')
return `# ${this.lint.path}\n\n${sectionText}`
return `# Database Advisor: Lint ${this.lint.path}\n\nThis is a database lint rule for Supabase, targeting the lint ID ${this.lint.path}. Lint rules help enforce performance and security best practices for your Supabase database.\n\n${sectionText}`
}
}
+1 -1
View File
@@ -118,7 +118,7 @@ combine-raw.tsdoc.v2:
generate: generate.sections.api.v1
generate.sections.api.v1:
node $(REPO_DIR)/sections/generateMgmtApiSections.ts $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/common-api-sections.json
npx tsx $(REPO_DIR)/sections/generateMgmtApiSections.cts $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/common-api-sections.json
###############################################################################
# Validate OpenAPI 3.0
+2 -2
View File
@@ -1,7 +1,7 @@
clispec: '001'
info:
id: cli
version: 2.30.4
version: 2.31.4
title: Supabase CLI
language: sh
source: https://github.com/supabase/cli
@@ -74,7 +74,7 @@ flags:
name: --workdir <string>
description: path to a Supabase project directory
default_value: ''
- id: "yes"
- id: 'yes'
name: --yes
description: answer yes to all prompts
default_value: 'false'
+20 -13
View File
@@ -3,7 +3,8 @@
"title": "Introduction",
"id": "introduction",
"slug": "introduction",
"type": "markdown"
"type": "markdown",
"items": []
},
{
"type": "category",
@@ -57,6 +58,12 @@
"type": "category",
"title": "Database",
"items": [
{
"id": "v1-apply-a-migration",
"title": "Apply a migration",
"slug": "v1-apply-a-migration",
"type": "operation"
},
{
"id": "v1-disable-readonly-mode-temporarily",
"title": "Disable readonly mode temporarily",
@@ -105,12 +112,6 @@
"slug": "v1-get-ssl-enforcement-config",
"type": "operation"
},
{
"id": "v1-get-supavisor-config",
"title": "Get supavisor config",
"slug": "v1-get-supavisor-config",
"type": "operation"
},
{
"id": "v1-list-all-backups",
"title": "List all backups",
@@ -123,6 +124,12 @@
"slug": "v1-list-all-snippets",
"type": "operation"
},
{
"id": "v1-list-migrations",
"title": "List migrations",
"slug": "v1-list-migrations",
"type": "operation"
},
{
"id": "v1-remove-a-read-replica",
"title": "Remove a read replica",
@@ -158,12 +165,6 @@
"title": "Update ssl enforcement config",
"slug": "v1-update-ssl-enforcement-config",
"type": "operation"
},
{
"id": "v1-update-supavisor-config",
"title": "Update supavisor config",
"slug": "v1-update-supavisor-config",
"type": "operation"
}
]
},
@@ -309,6 +310,12 @@
"slug": "v1-list-all-branches",
"type": "operation"
},
{
"id": "v1-merge-a-branch",
"title": "Merge a branch",
"slug": "v1-merge-a-branch",
"type": "operation"
},
{
"id": "v1-push-a-branch",
"title": "Push a branch",
@@ -490,6 +490,13 @@
"product": "auth",
"type": "function"
},
{
"id": "get-claims",
"title": "Get user claims from verified JWT",
"slug": "auth-getclaims",
"product": "auth",
"type": "function"
},
{
"id": "sign-out",
"title": "Sign out a user",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -41,7 +41,7 @@ function extractSectionsFromOpenApi(filePath, outputPath) {
for (const route in openApiJson.paths) {
const methods = openApiJson.paths[route]
for (const method in methods) {
const tag = methods[method].tags[0]
const tag = methods[method].tags?.[0]
const operationId = methods[method].operationId
// If operationId is not in the form of a slug ignore it.
// This is intentional because operationId is not defined under the swagger
+43
View File
@@ -1033,6 +1033,49 @@ functions:
window.location.href = data.url
}
```
- id: get-claims
title: 'getClaims()'
$ref: '@supabase/auth-js.GoTrueClient.getClaims'
examples:
- id: get-claims
name: Get user object
code: |
```js
const { data, error } = await supabase.auth.getClaims()
```
response: |
```json
{
"data": {
"claims": {
"aal": "aal1",
"amr": [{
"method": "email",
"timestamp": 1715766000
}],
"app_metadata": {},
"aud": "authenticated",
"email": "example@email.com",
"exp": 1715769600,
"iat": 1715766000,
"is_anonymous": false,
"iss": "https://api.supabase.com/auth/v1",
"phone": "+13334445555",
"role": "authenticated",
"session_id": "11111111-1111-1111-1111-111111111111",
"sub": "11111111-1111-1111-1111-111111111111",
"user_metadata": {}
},
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "abcdefgh"
},
"signature": [/** Uint8Array */],
},
"error": null
}
```
- id: sign-out
title: 'signOut()'
$ref: '@supabase/auth-js.GoTrueClient.signOut'
+22 -4
View File
@@ -969,7 +969,7 @@ functions:
```kotlin
supabase.from("countries").select {
filter {
filterNot("name", FilterOperation.IS, "")
filterNot("name", FilterOperator.IS, "")
}
}
```
@@ -3720,9 +3720,11 @@ functions:
$ref: '@supabase/gotrue-js.GoTrueClient.onAuthStateChange'
notes: |
Listen to session changes.
- `sessionStatus` is a `Flow<SessionStatus>` that emits the current session status. Everything related to the session is handled by this flow.
- `events` is a `Flow<AuthEvent>` that emits auth events. This flow is used to listen to auth events that are independent of the current session, like OTP errors, refresh failures, etc. Experimental.
examples:
- id: listen-to-auth-changes
name: Listen to auth changes
name: Listen to session status changes
isSpotlight: true
code: |
```kotlin
@@ -3742,7 +3744,9 @@ functions:
}
}
SessionStatus.Initializing -> println("Initializing")
is SessionStatus.RefreshFailure -> println("Refresh failure ${it.cause}") //Either a network error or a internal server error
is SessionStatus.RefreshFailure -> {
println("Session expired and could not be refreshed")
}
is SessionStatus.NotAuthenticated -> {
if(it.isSignOut) {
println("User signed out")
@@ -3759,6 +3763,20 @@ functions:
- `Initializing`,
- `RefreshFailure(cause)`,
- `Authenticated(session, source)`
- id: listen-to-events
name: Listen to auth events
isSpotlight: false
code: |
```kotlin
supabase.auth.events.collect {
when(it) {
is AuthEvent.OtpError ->
println("Found error in current URL / deeplink during the OAuth flow: ${it.error}")
is AuthEvent.RefreshFailure ->
println("Failed to refresh session: ${it.error}")
}
}
```
- id: reset-password-for-email
title: 'Send a password reset request'
notes: |
@@ -4053,7 +4071,7 @@ functions:
supabaseKey = "supabaseKey"
) {
install(Auth) {
minimalSettings() //disables session saving and auto-refreshing
minimalConfig() //disables session saving and auto-refreshing
}
// install other plugins (these will use the service role key)
}
+1 -1
View File
@@ -1222,7 +1222,7 @@ functions:
- name: attributes
isOptional: false
type: UserAttributes
subContent:
subContent:
- name: data
isOptional: true
type: object
@@ -6,12 +6,13 @@ import { formatForeignKeys } from 'components/interfaces/TableGridEditor/SidePan
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AlertError from 'components/ui/AlertError'
import { useForeignKeyConstraintsQuery } from 'data/database/foreign-key-constraints-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { Button, cn } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import type { Filter, GridProps, SupaRow } from '../../types'
import { useOnRowsChange } from './Grid.utils'
import RowRenderer from './RowRenderer'
@@ -67,6 +68,7 @@ export const Grid = memo(
}
const table = snap.table
const tableEntityType = snap.originalTable?.entity_type
const { mutate: sendEvent } = useSendEventMutation()
const org = useSelectedOrganization()
@@ -82,13 +84,13 @@ export const Grid = memo(
table?.columns.find((x) => x.name == columnName)?.foreignKey ?? {}
const fk = data?.find(
(key: any) =>
(key) =>
key.source_schema === table?.schema &&
key.source_table === table?.name &&
key.source_columns.includes(columnName) &&
key.target_schema === targetTableSchema &&
key.target_table === targetTableName &&
key.target_columns.includes(targetColumnName)
key.target_columns.includes(targetColumnName ?? '')
)
return fk !== undefined ? formatForeignKeys([fk])[0] : undefined
@@ -141,11 +143,15 @@ export const Grid = memo(
{(filters ?? []).length === 0 ? (
<div className="flex flex-col items-center justify-center col-span-full h-full">
<p className="text-sm text-light">This table is empty</p>
<p className="text-sm text-light mt-1">
Add rows to your table to get started.
</p>
<div className="flex items-center space-x-2 mt-4">
{
{tableEntityType === ENTITY_TYPE.FOREIGN_TABLE ? (
<div className="flex items-center space-x-2 mt-4">
<p className="text-sm text-light">
This table is a foreign table. Add data to the connected source to get
started.
</p>
</div>
) : (
<div className="flex items-center space-x-2 mt-4">
<Button
type="default"
className="pointer-events-auto"
@@ -163,8 +169,8 @@ export const Grid = memo(
>
Import data from CSV
</Button>
}
</div>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center col-span-full">
@@ -1,5 +1,6 @@
import { MoreVertical } from 'lucide-react'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { motion } from 'framer-motion'
import { MoreVertical } from 'lucide-react'
import {
Button,
DropdownMenu,
@@ -10,7 +11,6 @@ import {
} from 'ui'
import { APIKeyDeleteDialog } from './APIKeyDeleteDialog'
import { ApiKeyPill } from './ApiKeyPill'
import { APIKeysData } from 'data/api-keys/api-keys-query'
export const APIKeyRow = ({
apiKey,
@@ -34,14 +34,25 @@ export const APIKeyRow = ({
mass: 1,
}}
>
<TableCell className="py-2">{apiKey.name}</TableCell>
<TableCell className="py-2">
<div className="flex flex-col">
<span className="font-medium">{apiKey.name}</span>
<div className="text-sm text-foreground-lighter">
{apiKey.description || <span className="text-foreground-muted">No description</span>}
</div>
</div>
</TableCell>
<TableCell className="py-2">
<div className="flex flex-row gap-2">
<ApiKeyPill apiKey={apiKey} />
</div>
</TableCell>
<TableCell className="py-2">{apiKey.description || '/'}</TableCell>
<TableCell className="py-2">{lastSeen?.timestamp ?? '/'}</TableCell>
<TableCell className="py-2 min-w-0 whitespace-nowrap hidden lg:table-cell">
<div className="truncate" title={lastSeen?.timestamp || 'Never used'}>
{lastSeen?.timestamp ?? <span className="text-foreground-lighter">Never used</span>}
</div>
</TableCell>
<TableCell className="flex justify-end">
<DropdownMenu>
@@ -1,25 +0,0 @@
import { AnimatePresence } from 'framer-motion'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { APIKeyRow } from './APIKeyRow'
const APIKeysTable = ({
apiKeys,
}: {
apiKeys: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>[]
}) => {
return (
<table>
<thead />
<tbody>
<AnimatePresence initial={false}>
{apiKeys.map((apiKey) => (
<APIKeyRow key={apiKey.id} apiKey={apiKey} />
))}
</AnimatePresence>
</tbody>
</table>
)
}
export default APIKeysTable
@@ -1,10 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { AnimatePresence, motion } from 'framer-motion'
import { Eye } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
@@ -35,7 +35,6 @@ export function ApiKeyPill({
// It fetches the fully revealed API key when needed
const {
data,
isLoading: isLoadingApiKey,
error,
refetch: refetchApiKey,
} = useAPIKeyIdQuery(
@@ -106,7 +105,6 @@ export function ApiKeyPill({
// States for disabling buttons/showing tooltips
const isRestricted = isSecret && !canManageSecretKeys
const isLoading = isLoadingPermission
return (
<>
@@ -118,7 +116,7 @@ export function ApiKeyPill({
isSecret ? 'overflow-hidden' : '',
show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0',
'transition-all',
'max-w-[340px]',
'max-w-[100px] sm:max-w-[140px] md:max-w-[180px] lg:max-w-[340px]',
'cursor-text',
'relative'
)}
@@ -1,12 +1,14 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useMemo } from 'react'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useMemo } from 'react'
import { cn, EyeOffIcon, Input_Shadcn_, Skeleton, WarningIcon } from 'ui'
// to add in later with follow up PR
// import CreatePublishableAPIKeyDialog from './CreatePublishableAPIKeyDialog'
// to add in later with follow up PR
@@ -38,7 +40,7 @@ export const PublishableAPIKeys = () => {
description="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies."
/>
<div className="flex flex-col gap-8">
<div className="-space-y-px w-content w-fit">
<div className="-space-y-px w-full lg:w-content lg:w-fit">
<div className="bg-surface-100 px-5 py-2 flex items-center gap-5 first:rounded-t-md border">
<span className="text-sm">Publishable key</span>
<div className="flex items-center gap-2">
@@ -59,7 +61,7 @@ export const PublishableAPIKeys = () => {
</div>
) : (
<div className="text-xs bg-200 last:rounded-b-md border px-5 text-foreground-lighter py-1">
Publishable key can be safely shared in public
The publishable key can be safely shared publicly
</div>
)}
</div>
@@ -91,7 +93,8 @@ function ApiKeyInput() {
// The default publisahble key will always be the first one
const apiKey = publishableApiKeys[0]
const baseClasses = 'flex-1 grow gap-1 rounded-full min-w-[32em]'
const baseClasses =
'flex-1 grow gap-1 rounded-full min-w-0 max-w-[200px] sm:max-w-[300px] md:max-w-[400px] lg:min-w-[24rem]'
const size = 'tiny'
if (isApiKeysLoading || isPermissionsLoading) {
@@ -112,10 +112,8 @@ export const SecretAPIKeys = () => {
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 pr-0">
API Key
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
Description
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2">
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 hidden lg:table-cell">
Last Seen
</TableHead>
<TableHead className="text-right font-mono uppercase text-xs text-foreground-lighter h-auto py-2" />
@@ -11,6 +11,7 @@ import { useProjectLintRulesQuery } from 'data/lint/lint-rules-query'
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import {
Badge,
Button,
Card,
CardContent,
@@ -24,11 +25,19 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { LintInfo } from '../Linter/Linter.constants'
import { generateRuleText } from './AdvisorRules.utils'
import { CreateRuleSheet } from './CreateRuleSheet'
import { DisableRuleModal } from './DisableRuleModal'
import { EnableRuleModal } from './EnableRuleModal'
interface AdvisorRuleItemProps {
lint: LintInfo
}
// [Joshen] Context: We're going with a simplified interface for LW15 as we launch
// this as a feature preview. Going to be purely disabling rules for the whole project,
// not assigning to members, etc (all these can come in the future when we're ready)
// Hence using this to clearly indicate where is being simplified
const SIMPLIFIED_INTERFACE = true
export const AdvisorRuleItem = ({ lint }: AdvisorRuleItemProps) => {
const { ref: projectRef } = useParams()
const organization = useSelectedOrganization()
@@ -63,6 +72,30 @@ export const AdvisorRuleItem = ({ lint }: AdvisorRuleItemProps) => {
deleteRule({ projectRef, ids: [selectedRuleToDelete] })
}
if (SIMPLIFIED_INTERFACE) {
return (
<Card className="border-b-0 rounded-none last:border-b first:rounded-t-md last:rounded-b-md">
<CardContent className="py-3 flex items-center justify-between text-sm gap-4 cursor-pointer transition hover:bg-surface-200">
<div className="flex items-center justify-center [&>svg]:text-foreground-lighter">
{lint.icon}
</div>
<div className="flex-1 flex items-center gap-x-2">
<span>{lint.title}</span>
{rules.length > 0 && <Badge>Disabled</Badge>}
</div>
<div className="flex items-center gap-x-2">
<DocsButton href={lint.docsLink} />
{rules.length > 0 ? (
<EnableRuleModal lint={lint} rule={rules[0]} />
) : (
<DisableRuleModal lint={lint} />
)}
</div>
</CardContent>
</Card>
)
}
return (
<>
<Collapsible_Shadcn_
@@ -0,0 +1,90 @@
import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useLintRuleCreateMutation } from 'data/lint/create-lint-rule-mutation'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
import { LintInfo } from '../Linter/Linter.constants'
import { lintInfoMap } from '../Linter/Linter.utils'
interface DisableRuleModalProps {
lint: LintInfo
}
export const DisableRuleModal = ({ lint }: DisableRuleModalProps) => {
const { ref } = useParams()
const router = useRouter()
const routeCategory = router.pathname.split('/').pop()
const [open, setOpen] = useState(false)
const { mutate: createRule, isLoading: isCreating } = useLintRuleCreateMutation({
onSuccess: (_, vars) => {
const ruleLint = vars.exception.lint_name
const ruleLintMeta = lintInfoMap.find((x) => x.name === ruleLint)
toast.success(`Successfully disabled the "${ruleLintMeta?.title}" rule`)
if (ruleLintMeta) {
if (!!routeCategory && routeCategory !== ruleLintMeta.category) {
router.push(
`/project/${ref}/advisors/rules/${ruleLintMeta.category}?lint=${ruleLintMeta.name}`
)
}
}
setOpen(false)
},
})
const onCreateRule = () => {
if (!ref) return console.error('Project ref is required')
createRule({
projectRef: ref,
exception: {
is_disabled: true,
lint_category: undefined,
lint_name: lint.name,
assigned_to: undefined,
},
})
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button type="default">Disable rule</Button>
</DialogTrigger>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Confirm to disable rule</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection>
<p className="text-sm">
This will silence the "{lint.title}" by hiding this rule in the Advisor reports, as well
omitting this rule from email notifications for this project.
</p>
</DialogSection>
<DialogFooter>
<Button disabled={isCreating} type="default" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button loading={isCreating} type="primary" onClick={onCreateRule}>
Disable
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,71 @@
import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useLintRuleDeleteMutation } from 'data/lint/delete-lint-rule-mutation'
import { LintException } from 'data/lint/lint-rules-query'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
import { LintInfo } from '../Linter/Linter.constants'
interface EnableRuleModalProps {
lint: LintInfo
rule: LintException
}
export const EnableRuleModal = ({ lint, rule }: EnableRuleModalProps) => {
const { ref } = useParams()
const router = useRouter()
const [open, setOpen] = useState(false)
const { mutate: deleteRule, isLoading: isDeleting } = useLintRuleDeleteMutation({
onSuccess: () => {
toast.success(`Successfully enabled the "${lint.title}" rule`)
setOpen(false)
},
})
const onDeleteRule = () => {
if (!ref) return console.error('Project ref is required')
deleteRule({ projectRef: ref, ids: [rule.id] })
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button type="default">Enable rule</Button>
</DialogTrigger>
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Confirm to enable rule</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection>
<p className="text-sm">
The "{lint.title}" rule will be visible in the Advisor reports, and will be included in
email notifications for this project.
</p>
</DialogSection>
<DialogFooter>
<Button disabled={isDeleting} type="default" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button loading={isDeleting} type="primary" onClick={onDeleteRule}>
Enable
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,45 @@
import Image from 'next/image'
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { BASE_PATH } from 'lib/constants'
import { useIsRealtimeSettingsEnabled } from './FeaturePreviewContext'
export const AdvisorRulesPreview = () => {
const { ref } = useParams()
const isRealtimeSettingsEnabled = useIsRealtimeSettingsEnabled()
return (
<div>
<p className="text-sm text-foreground-light mb-4">
Disable specific Advisor categories or rules to prevent them from showing up in Advisor
reports or email notifications. This could be useful for rules that might not be applicable
to your project.
</p>
<Image
src={`${BASE_PATH}/img/previews/advisor-rule-preview.png`}
width={1296}
height={900}
alt="api-docs-side-panel-preview"
className="rounded border mb-4"
/>
<div className="space-y-2 !mt-4">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
Allow you to disable advisor rules for your project from the{' '}
<InlineLink
href={
isRealtimeSettingsEnabled
? `/project/${ref}/advisors/security`
: `/project/${ref}/advisors/rules/security`
}
>
Advisors section.
</InlineLink>
</li>
</ul>
</div>
</div>
)
}
@@ -0,0 +1,64 @@
import Image from 'next/image'
import { InlineLink } from 'components/ui/InlineLink'
import { BASE_PATH } from 'lib/constants'
export const Branching2Preview = () => {
return (
<div>
<Image
src={`${BASE_PATH}/img/previews/branching-preview.png`}
width={1296}
height={900}
alt="api-docs-side-panel-preview"
className="rounded border mb-4"
/>
<p className="text-sm text-foreground-light mb-4">
Branching 2.0 introduces a new workflow for managing database branches without having to use
Git. Create branches, review changes and merge back into production all through the
dashboard. Read the below limitations and our{' '}
<InlineLink href="https://supabase.com/docs/guides/platform/branching">
branching documentation
</InlineLink>{' '}
before opting in.
</p>
<div className="my-6">
<p className="text-sm text-foreground mb-2 font-medium">Limitations:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>Custom roles created through the dashboard are not captured on branch creation.</li>
<li>
Only <code>public</code> schema changes are supported right now.
</li>
<li>Extensions are not included in the diff process</li>
<li>
Branches can only be merged to <code>main</code>; merging between preview branches is
not supported.
</li>
<li>
If your branch is out of date, you can pull in latest changes from <code>main</code>,
but keep in mind that all functions will be overwritten.
</li>
<li>
Deleting functions must be done manually on <code>main</code>.
</li>
<li>Migration conflicts must be manually resolved on the preview branch.</li>
<li>
If you have run migrations on <code>main</code>, new branches will be created from
existing migrations instead of a full schema dump.
</li>
</ul>
</div>
<div className="space-y-2 !mt-4">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>Enable the new Branching 2.0 workflow for your project.</li>
<li>
Allow you to create, manage, and merge database branches with improved UI and features.
</li>
<li>Access new merge request and deployment management tools.</li>
</ul>
</div>
</div>
)
}
@@ -1,6 +1,20 @@
import { LOCAL_STORAGE_KEYS } from 'common'
export const FEATURE_PREVIEWS = [
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS,
name: 'New Logs Interface',
discussionsUrl: 'https://github.com/orgs/supabase/discussions/37234',
isNew: true,
isPlatformOnly: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0,
name: 'Branching 2.0',
discussionsUrl: 'https://github.com/orgs/supabase/discussions/branching-2-0',
isNew: true,
isPlatformOnly: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_REALTIME_SETTINGS,
name: 'Realtime settings',
@@ -8,6 +22,13 @@ export const FEATURE_PREVIEWS = [
isNew: true,
isPlatformOnly: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES,
name: 'Disable Advisor rules',
discussionsUrl: undefined,
isNew: true,
isPlatformOnly: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR,
name: 'Directly edit database entities',
@@ -1,7 +1,17 @@
import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { useQueryState } from 'nuqs'
import {
PropsWithChildren,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { FeatureFlagContext, LOCAL_STORAGE_KEYS } from 'common'
import { useFlag, useIsRealtimeSettingsFFEnabled } from 'hooks/ui/useFlag'
import { EMPTY_OBJ } from 'lib/void'
import { FEATURE_PREVIEWS } from './FeaturePreview.constants'
@@ -80,7 +90,111 @@ export const useIsInlineEditorEnabled = () => {
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR]
}
export const useUnifiedLogsPreview = () => {
const { flags, onUpdateFlag } = useFeaturePreviewContext()
const isEnabled = flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]
const enable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, true)
const disable = () => onUpdateFlag(LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS, false)
return { isEnabled, enable, disable }
}
export const useIsRealtimeSettingsEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_REALTIME_SETTINGS]
}
export const useIsBranching2Enabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]
}
export const useIsAdvisorRulesEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]
}
export const useFeaturePreviewModal = () => {
const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal')
const isRealtimeSettingsEnabled = useIsRealtimeSettingsFFEnabled()
const gitlessBranchingEnabled = useFlag('gitlessBranching')
const advisorRulesEnabled = useFlag('advisorRules')
const isUnifiedLogsPreviewAvailable = useFlag('unifiedLogs')
const selectedFeatureKeyFromQuery = featurePreviewModal?.trim() ?? null
const showFeaturePreviewModal = selectedFeatureKeyFromQuery !== null
// [Joshen] Use this if we want to feature flag previews
const isFeaturePreviewReleasedToPublic = useCallback(
(feature: (typeof FEATURE_PREVIEWS)[number]) => {
switch (feature.key) {
case 'supabase-ui-realtime-settings':
return isRealtimeSettingsEnabled
case 'supabase-ui-branching-2-0':
return gitlessBranchingEnabled
case 'supabase-ui-advisor-rules':
return advisorRulesEnabled
case 'supabase-ui-preview-unified-logs':
return isUnifiedLogsPreviewAvailable
default:
return true
}
},
[
isRealtimeSettingsEnabled,
gitlessBranchingEnabled,
advisorRulesEnabled,
isUnifiedLogsPreviewAvailable,
]
)
const selectedFeatureKey = (
!selectedFeatureKeyFromQuery
? FEATURE_PREVIEWS.filter((feature) => isFeaturePreviewReleasedToPublic(feature))[0].key
: selectedFeatureKeyFromQuery
) as (typeof FEATURE_PREVIEWS)[number]['key']
const selectFeaturePreview = useCallback(
(featureKey: (typeof FEATURE_PREVIEWS)[number]['key']) => {
setFeaturePreviewModal(featureKey)
},
[setFeaturePreviewModal]
)
const openFeaturePreviewModal = useCallback(() => {
selectFeaturePreview(selectedFeatureKey)
}, [selectFeaturePreview, selectedFeatureKey])
const closeFeaturePreviewModal = useCallback(() => {
setFeaturePreviewModal(null)
}, [setFeaturePreviewModal])
const toggleFeaturePreviewModal = useCallback(() => {
if (showFeaturePreviewModal) {
closeFeaturePreviewModal()
} else {
openFeaturePreviewModal()
}
}, [showFeaturePreviewModal, openFeaturePreviewModal, closeFeaturePreviewModal])
return useMemo(
() => ({
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
openFeaturePreviewModal,
closeFeaturePreviewModal,
toggleFeaturePreviewModal,
isFeaturePreviewReleasedToPublic,
}),
[
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
openFeaturePreviewModal,
closeFeaturePreviewModal,
toggleFeaturePreviewModal,
isFeaturePreviewReleasedToPublic,
]
)
}
@@ -5,52 +5,46 @@ import { ReactNode } from 'react'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useIsRealtimeSettingsFFEnabled } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
import { AdvisorRulesPreview } from './AdvisorRulesPreview'
import { APISidePanelPreview } from './APISidePanelPreview'
import { Branching2Preview } from './Branching2Preview'
import { CLSPreview } from './CLSPreview'
import { FEATURE_PREVIEWS } from './FeaturePreview.constants'
import { useFeaturePreviewContext } from './FeaturePreviewContext'
import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
import { InlineEditorPreview } from './InlineEditorPreview'
import { RealtimeSettingsPreview } from './RealtimeSettingsPreview'
import { UnifiedLogsPreview } from './UnifiedLogsPreview'
const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[key: string]: ReactNode
} = {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_BRANCHING_2_0]: <Branching2Preview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_REALTIME_SETTINGS]: <RealtimeSettingsPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_ADVISOR_RULES]: <AdvisorRulesPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR]: <InlineEditorPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: <APISidePanelPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: <CLSPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_UNIFIED_LOGS]: <UnifiedLogsPreview />,
}
const FeaturePreviewModal = () => {
const { ref } = useParams()
const snap = useAppStateSnapshot()
const {
showFeaturePreviewModal,
selectedFeatureKey,
selectFeaturePreview,
closeFeaturePreviewModal,
isFeaturePreviewReleasedToPublic,
} = useFeaturePreviewModal()
const org = useSelectedOrganization()
const featurePreviewContext = useFeaturePreviewContext()
const { mutate: sendEvent } = useSendEventMutation()
const isRealtimeSettingsEnabled = useIsRealtimeSettingsFFEnabled()
// [Joshen] Use this if we want to feature flag previews
function isReleasedToPublic(feature: (typeof FEATURE_PREVIEWS)[number]) {
switch (feature.key) {
case 'supabase-ui-realtime-settings':
return isRealtimeSettingsEnabled
default:
return true
}
}
const selectedFeatureKey =
snap.selectedFeaturePreview === ''
? FEATURE_PREVIEWS.filter((feature) => isReleasedToPublic(feature))[0].key
: snap.selectedFeaturePreview
const { flags, onUpdateFlag } = featurePreviewContext
const selectedFeature = FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey)
const selectedFeature =
FEATURE_PREVIEWS.find((preview) => preview.key === selectedFeatureKey) ?? FEATURE_PREVIEWS[0]
const isSelectedFeatureEnabled = flags[selectedFeatureKey]
const allFeaturePreviews = IS_PLATFORM
@@ -58,18 +52,14 @@ const FeaturePreviewModal = () => {
: FEATURE_PREVIEWS.filter((x) => !x.isPlatformOnly)
const toggleFeature = () => {
onUpdateFlag(selectedFeatureKey, !isSelectedFeatureEnabled)
onUpdateFlag(selectedFeature.key, !isSelectedFeatureEnabled)
sendEvent({
action: isSelectedFeatureEnabled ? 'feature_preview_disabled' : 'feature_preview_enabled',
properties: { feature: selectedFeatureKey },
properties: { feature: selectedFeature.key },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}
function handleCloseFeaturePreviewModal() {
snap.setShowFeaturePreviewModal(false)
}
return (
<Modal
hideFooter
@@ -77,25 +67,25 @@ const FeaturePreviewModal = () => {
size="xlarge"
className="!max-w-4xl"
header="Dashboard feature previews"
visible={snap.showFeaturePreviewModal}
onCancel={handleCloseFeaturePreviewModal}
visible={showFeaturePreviewModal}
onCancel={closeFeaturePreviewModal}
>
{FEATURE_PREVIEWS.length > 0 ? (
<div className="flex">
<div>
<ScrollArea className="h-[550px] w-[280px] border-r">
{allFeaturePreviews
.filter((feature) => isReleasedToPublic(feature))
.filter((feature) => isFeaturePreviewReleasedToPublic(feature))
.map((feature) => {
const isEnabled = flags[feature.key] ?? false
return (
<div
key={feature.key}
onClick={() => snap.setSelectedFeaturePreview(feature.key)}
onClick={() => selectFeaturePreview(feature.key)}
className={cn(
'flex items-center space-x-3 p-4 border-b cursor-pointer bg transition',
selectedFeatureKey === feature.key ? 'bg-surface-300' : 'bg-surface-100'
selectedFeature.key === feature.key ? 'bg-surface-300' : 'bg-surface-100'
)}
>
{isEnabled ? (
@@ -130,7 +120,7 @@ const FeaturePreviewModal = () => {
</Button>
</div>
</div>
{FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeatureKey]}
{FEATURE_PREVIEW_KEY_TO_CONTENT[selectedFeature.key]}
</div>
</div>
) : (
@@ -0,0 +1,36 @@
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { BASE_PATH } from 'lib/constants'
import Image from 'next/image'
export const UnifiedLogsPreview = () => {
const { ref } = useParams()
return (
<div className="space-y-2">
<Image
alt="new-logs-preview"
src={`${BASE_PATH}/img/previews/new-logs-preview.png`}
width={1296}
height={900}
className="rounded border mb-4"
/>
<p className="text-foreground-light text-sm mb-4">
Experience our enhanced logs interface with improved filtering, real-time updates, and a
unified view across all your services. Built for better performance and easier debugging.
</p>
<div className="space-y-2 !mt-4">
<p className="text-sm">Enabling this preview will:</p>
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
<li>
Replace the current logs interface on the{' '}
<InlineLink href={`/project/${ref}/logs`}>logs page</InlineLink> with a unified view
</li>
<li>Provide enhanced filtering capabilities and real-time log streaming</li>
<li>Improve performance with optimized data loading and virtualization</li>
<li>Offer a more modern interface with better search and navigation</li>
</ul>
</div>
</div>
)
}
@@ -2,7 +2,7 @@ import { isEmpty, noop } from 'lodash'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useAppStateSnapshot } from 'state/app-state'
import { useFeaturePreviewModal } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Modal } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { POLICY_MODAL_VIEWS } from '../Policies.constants'
@@ -47,7 +47,7 @@ const PolicyEditorModal = ({
onUpdatePolicy,
onSaveSuccess = noop,
}: PolicyEditorModalProps) => {
const snap = useAppStateSnapshot()
const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
const newPolicyTemplate: PolicyFormField = {
schema,
@@ -95,7 +95,7 @@ const PolicyEditorModal = ({
const onSelectBackFromTemplates = () => setView(previousView)
const onToggleFeaturePreviewModal = () => {
snap.setShowFeaturePreviewModal(!snap.showFeaturePreviewModal)
toggleFeaturePreviewModal()
onSelectCancel()
}
@@ -1,440 +0,0 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { GitMerge, MessageCircle } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { GitHubStatus } from 'components/interfaces/Settings/Integrations/GithubIntegration/GitHubStatus'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { useBranchDeleteMutation } from 'data/branches/branch-delete-mutation'
import { useBranchesDisableMutation } from 'data/branches/branches-disable-mutation'
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useFlag } from 'hooks/ui/useFlag'
import { useUrlState } from 'hooks/ui/useUrlState'
import { useAppStateSnapshot } from 'state/app-state'
import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { BranchLoader, BranchManagementSection, BranchRow } from './BranchPanels'
import { BranchSelector } from './BranchSelector'
import { PreviewBranchesEmptyState, PullRequestsEmptyState } from './EmptyStates'
import { Overview } from './Overview'
import { ReviewRow } from './ReviewRow'
type Tab = 'overview' | 'prs' | 'branches'
const BranchManagement = () => {
const router = useRouter()
const { ref } = useParams()
const project = useSelectedProject()
const selectedOrg = useSelectedOrganization()
const gitlessBranching = useFlag('gitlessBranching')
const hasBranchEnabled = project?.is_branch_enabled
const isBranch = project?.parent_project_ref !== undefined
const projectRef =
project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
const [urlParams, setParams] = useUrlState<{ tab: Tab }>()
const tab = urlParams.tab ?? 'overview'
const setTab = (tab: Tab) => setParams({ tab })
const snap = useAppStateSnapshot()
const [showDisableBranching, setShowDisableBranching] = useState(false)
const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
const canReadBranches = useCheckPermissions(PermissionAction.READ, 'preview_branches')
const canCreateBranches = useCheckPermissions(PermissionAction.CREATE, 'preview_branches', {
resource: { is_default: false },
})
const {
data: connections,
error: connectionsError,
isLoading: isLoadingConnections,
isSuccess: isSuccessConnections,
isError: isErrorConnections,
} = useGitHubConnectionsQuery({
organizationId: selectedOrg?.id,
})
const {
data: branches,
error: branchesError,
isLoading: isLoadingBranches,
isError: isErrorBranches,
isSuccess: isSuccessBranches,
} = useBranchesQuery({ projectRef })
const [[mainBranch], previewBranchesUnsorted] = partition(branches, (branch) => branch.is_default)
const previewBranches = previewBranchesUnsorted.sort((a, b) =>
new Date(a.updated_at) < new Date(b.updated_at) ? 1 : -1
)
const branchesWithPRs = previewBranches.filter((branch) => branch.pr_number !== undefined)
const branchesReadyForReview = previewBranches.filter(
(branch) => branch.review_requested_at !== undefined && branch.review_requested_at !== null
)
const currentBranch = branches?.find((branch) => branch.project_ref === ref)
const isCurrentBranchReadyForReview =
currentBranch?.review_requested_at !== undefined && currentBranch?.review_requested_at !== null
const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
const repo = githubConnection?.repository.name ?? ''
const isError = isErrorConnections || isErrorBranches
const isLoading = isLoadingConnections || isLoadingBranches
const isSuccess = isSuccessConnections && isSuccessBranches
const { mutate: deleteBranch, isLoading: isDeleting } = useBranchDeleteMutation({
onSuccess: () => {
if (selectedBranchToDelete?.project_ref === ref) {
toast.success(
'Successfully deleted branch. You are now currently on the main branch of your project.'
)
router.push(`/project/${projectRef}/branches`)
} else {
toast.success('Successfully deleted branch')
}
setSelectedBranchToDelete(undefined)
},
})
const { mutate: disableBranching, isLoading: isDisabling } = useBranchesDisableMutation({
onSuccess: () => {
toast.success('Successfully disabled branching for project')
setShowDisableBranching(false)
},
})
const { mutate: updateBranch, isLoading: isUpdating } = useBranchUpdateMutation({
onError: () => {
toast.error(`Failed to update the branch`)
},
})
const handleMarkBranchForReview = (branch: Branch) => {
if (branch.id && projectRef) {
updateBranch(
{
id: branch.id,
projectRef,
requestReview: true,
},
{
onSuccess: () => {
toast.success('Branch marked as ready for review')
router.push(`/project/${branch.project_ref}/merge`)
},
}
)
}
}
const generateCreatePullRequestURL = (branch?: string) => {
if (githubConnection === undefined) return 'https://github.com'
return branch !== undefined
? `https://github.com/${githubConnection.repository.name}/compare/${mainBranch?.git_branch}...${branch}`
: `https://github.com/${githubConnection.repository.name}/compare`
}
const onConfirmDeleteBranch = () => {
if (selectedBranchToDelete == undefined) return console.error('No branch selected')
if (projectRef == undefined) return console.error('Project ref is required')
deleteBranch({ id: selectedBranchToDelete?.id, projectRef })
}
const onConfirmDisableBranching = () => {
if (projectRef == undefined) return console.error('Project ref is required')
if (!previewBranches) return console.error('No branches available')
disableBranching({ projectRef, branchIds: previewBranches?.map((branch) => branch.id) })
}
return (
<>
<ScaffoldContainer>
<ScaffoldSection>
<div className="col-span-12">
<h3 className="text-xl mb-8">Branches</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center -space-x-px">
<Button
type="default"
className={`rounded-r-none transition hover:opacity-90 ${
tab === 'overview' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setTab('overview')}
>
Overview
</Button>
<Button
type="default"
className={`rounded-none transition hover:opacity-90 ${
tab === 'prs' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setTab('prs')}
>
Merge requests
</Button>
<Button
type="default"
className={`rounded-l-none transition hover:opacity-90 ${
tab === 'branches' ? 'opacity-100' : 'opacity-60'
}`}
onClick={() => setTab('branches')}
>
All branches
</Button>
</div>
<div className="flex items-center justify-between gap-x-2">
<Button
asChild
type="text"
icon={<MessageCircle className="text-muted" strokeWidth={1} />}
>
<a
target="_blank"
rel="noreferrer"
href="https://github.com/orgs/supabase/discussions/18937"
>
Branching Feedback
</a>
</Button>
<DocsButton href="https://supabase.com/docs/guides/platform/branching" />
<ButtonTooltip
type="primary"
disabled={!canCreateBranches}
onClick={() => snap.setShowCreateBranchModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canCreateBranches
? 'You need additional permissions to create branches'
: undefined,
},
}}
>
Create branch
</ButtonTooltip>
</div>
</div>
{!canReadBranches ? (
<NoPermission resourceText="view this project's branches" />
) : (
<>
{isErrorConnections && (
<AlertError
error={connectionsError}
subject="Failed to retrieve GitHub integration connection"
/>
)}
{isSuccessConnections && hasBranchEnabled && <GitHubStatus />}
{isErrorBranches && tab === 'overview' && (
<AlertError
error={branchesError}
subject="Failed to retrieve preview branches"
/>
)}
{!isError && (
<>
{tab === 'overview' && (
<Overview
isLoading={isLoading}
isSuccess={isSuccess}
repo={repo}
mainBranch={mainBranch}
previewBranches={previewBranches}
onViewAllBranches={() => setTab('branches')}
onSelectCreateBranch={() => snap.setShowCreateBranchModal(true)}
onSelectDeleteBranch={setSelectedBranchToDelete}
generateCreatePullRequestURL={generateCreatePullRequestURL}
showProductionBranch={hasBranchEnabled}
/>
)}
{tab === 'prs' && (
<div className="space-y-4">
{gitlessBranching && (
<BranchManagementSection
header={
<div className="flex items-center justify-between w-full">
<span>
{branchesReadyForReview.length} branches ready for review
</span>
<BranchSelector
branches={previewBranches}
onBranchSelected={handleMarkBranchForReview}
disabled={!projectRef}
isUpdating={isUpdating}
/>
</div>
}
>
{isBranch && !isCurrentBranchReadyForReview && currentBranch && (
<div className="bg-background px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-foreground-light">
<GitMerge
strokeWidth={1.5}
size={16}
className="text-brand"
/>
Mark{' '}
<span className="text-foreground">{currentBranch.name}</span>{' '}
ready for review
</div>
<Button
type="primary"
size="tiny"
onClick={() =>
currentBranch && handleMarkBranchForReview(currentBranch)
}
>
Ready for review
</Button>
</div>
</div>
)}
{branchesReadyForReview.length > 0 ? (
branchesReadyForReview.map((branch) => {
return <ReviewRow key={branch.id} branch={branch} />
})
) : (
<div className="px-6 py-4 text-sm text-foreground-light">
No branches are currently ready for review
</div>
)}
</BranchManagementSection>
)}
<BranchManagementSection
header={`${branchesWithPRs.length} branches with pull requests`}
>
{branchesWithPRs.length > 0 ? (
branchesWithPRs.map((branch) => {
return (
<BranchRow
key={branch.id}
repo={repo}
branch={branch}
generateCreatePullRequestURL={generateCreatePullRequestURL}
onSelectDeleteBranch={() => setSelectedBranchToDelete(branch)}
/>
)
})
) : (
<PullRequestsEmptyState
url={generateCreatePullRequestURL()}
projectRef={projectRef ?? '_'}
hasBranches={previewBranches.length > 0}
githubConnection={githubConnection}
gitlessBranching={gitlessBranching}
/>
)}
</BranchManagementSection>
</div>
)}
{tab === 'branches' && (
<BranchManagementSection
header={`${previewBranches.length} branches found`}
>
{isLoadingBranches && <BranchLoader />}
{isErrorBranches && (
<AlertError
error={branchesError}
subject="Failed to retrieve preview branches"
/>
)}
{isSuccessBranches && previewBranches.length === 0 && (
<PreviewBranchesEmptyState
onSelectCreateBranch={() => snap.setShowCreateBranchModal(true)}
/>
)}
{isSuccessBranches &&
previewBranches.map((branch) => {
return (
<BranchRow
key={branch.id}
repo={repo}
branch={branch}
generateCreatePullRequestURL={generateCreatePullRequestURL}
onSelectDeleteBranch={() => setSelectedBranchToDelete(branch)}
/>
)
})}
</BranchManagementSection>
)}
</>
)}
</>
)}
</div>
</div>
</ScaffoldSection>
</ScaffoldContainer>
<TextConfirmModal
variant={'warning'}
visible={selectedBranchToDelete !== undefined}
onCancel={() => setSelectedBranchToDelete(undefined)}
onConfirm={() => onConfirmDeleteBranch()}
loading={isDeleting}
title="Delete branch"
confirmLabel="Delete branch"
confirmPlaceholder="Type in name of branch"
confirmString={selectedBranchToDelete?.name ?? ''}
alert={{ title: 'You cannot recover this branch once deleted' }}
text={
<>
This will delete your database preview branch{' '}
<span className="text-bold text-foreground">{selectedBranchToDelete?.name}</span>.
</>
}
/>
<ConfirmationModal
variant={'destructive'}
size="medium"
loading={isDisabling}
visible={showDisableBranching}
title="Confirm disable branching for project"
confirmLabel="Confirm disable branching"
confirmLabelLoading="Disabling branching..."
onConfirm={() => onConfirmDisableBranching()}
onCancel={() => setShowDisableBranching(false)}
alert={{
title: 'This action cannot be undone',
description:
'All database preview branches will be removed upon disabling branching. You may still re-enable branching again thereafter, but your existing preview branches will not be restored.',
}}
>
<p className="text-sm">Before you disable branching, consider:</p>
<ul className="space-y-2 mt-2 text-sm text-foreground-light">
<li className="list-disc ml-6">Your project no longer requires database previews.</li>
<li className="list-disc ml-6">
None of your database previews are currently being used in any app.
</li>
</ul>
</ConfirmationModal>
</>
)
}
export default BranchManagement
@@ -1,47 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import {
ArrowRight,
Clock,
ExternalLink,
GitPullRequest,
Github,
Infinity,
MoreVertical,
Pencil,
RefreshCw,
Shield,
Trash2,
} from 'lucide-react'
import Link from 'next/link'
import { PropsWithChildren, ReactNode, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { toast } from 'sonner'
import { Github } from 'lucide-react'
import { useRouter } from 'next/router'
import { PropsWithChildren, ReactNode } from 'react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useBranchQuery } from 'data/branches/branch-query'
import { useBranchResetMutation } from 'data/branches/branch-reset-mutation'
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
import type { Branch } from 'data/branches/branches-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import {
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import BranchStatusBadge from './BranchStatusBadge'
import { EditBranchModal } from './EditBranchModal'
import WorkflowLogs from './WorkflowLogs'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { WorkflowLogs } from './WorkflowLogs'
interface BranchManagementSectionProps {
header: string | ReactNode
@@ -54,11 +20,13 @@ export const BranchManagementSection = ({
children,
}: PropsWithChildren<BranchManagementSectionProps>) => {
return (
<div className="border rounded-lg">
<div className="bg-surface-100 shadow-sm flex justify-between items-center px-6 py-2 rounded-t-lg text-sm">
<div className="border rounded-lg overflow-hidden">
<div className="bg-surface-100 shadow-sm flex justify-between items-center px-4 py-3 rounded-t-lg text-xs font-mono uppercase">
{typeof header === 'string' ? <span>{header}</span> : header}
</div>
<div className="bg-surface border-t shadow-sm rounded-b-lg text-sm divide-y">{children}</div>
<div className="bg-surface border-t shadow-sm rounded-b-lg text-sm divide-y px-4">
{children}
</div>
{footer !== undefined && <div className="bg-surface-100 px-6 py-1 border-t">{footer}</div>}
</div>
)
@@ -93,344 +61,78 @@ export const BranchLoader = () => {
interface BranchRowProps {
repo: string
label?: string | ReactNode
branch: Branch
isMain?: boolean
generateCreatePullRequestURL?: (branchName?: string) => string
onSelectDeleteBranch: () => void
rowLink?: string
external?: boolean
rowActions?: ReactNode
}
export const BranchRow = ({
branch,
isMain = false,
label,
repo,
generateCreatePullRequestURL,
onSelectDeleteBranch,
rowLink,
external = false,
rowActions,
}: BranchRowProps) => {
const { ref: projectRef } = useParams()
const isActive = projectRef === branch?.project_ref
const gitlessBranching = useFlag('gitlessBranching')
const canDeleteBranches = useCheckPermissions(PermissionAction.DELETE, 'preview_branches')
const canUpdateBranches = useCheckPermissions(PermissionAction.UPDATE, 'preview_branches')
const router = useRouter()
const page = router.pathname.split('/').pop()
const daysFromNow = dayjs().diff(dayjs(branch.updated_at), 'day')
const formattedTimeFromNow = dayjs(branch.updated_at).fromNow()
const formattedUpdatedAt = dayjs(branch.updated_at).format('DD MMM YYYY, HH:mm:ss (ZZ)')
const createPullRequestURL =
generateCreatePullRequestURL?.(branch.git_branch) ?? 'https://github.com'
const navigateUrl = rowLink ?? `/project/${branch.project_ref}`
const { ref, inView } = useInView()
const { data } = useBranchQuery(
{ projectRef, id: branch.id },
{
enabled: inView,
refetchInterval(data) {
if (data?.status !== 'ACTIVE_HEALTHY') {
return 1000 * 3 // 3 seconds
}
return false
},
const handleRowClick = () => {
if (external) {
window.open(navigateUrl, '_blank', 'noopener noreferrer')
} else {
router.push(navigateUrl)
}
)
const isBranchActiveHealthy = data?.status === 'ACTIVE_HEALTHY'
const [showConfirmResetModal, setShowConfirmResetModal] = useState(false)
const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false)
const [showEditBranchModal, setShowEditBranchModal] = useState(false)
const { mutate: updateBranch, isLoading: isUpdating } = useBranchUpdateMutation({
onSuccess() {
toast.success('Successfully updated branch')
setShowBranchModeSwitch(false)
},
})
const onUpdateBranchPersistentMode = () => {
if (projectRef == undefined) return console.error('Project ref is required')
updateBranch({ id: branch.id, projectRef, persistent: !branch.persistent })
}
const { mutate: resetBranch, isLoading: isResetting } = useBranchResetMutation({
onSuccess() {
toast.success('Success! Please allow a few seconds for the branch to reset.')
setShowConfirmResetModal(false)
},
})
function onConfirmReset() {
if (!projectRef) throw new Error('Invalid project reference')
resetBranch({ id: branch.id, projectRef })
}
return (
<div className="w-full flex items-center justify-between px-6 py-2.5" ref={ref}>
<div className="w-full flex items-center justify-between px-4 py-2.5 hover:bg-surface-100">
<div className="flex items-center gap-x-3">
{branch.git_branch && (
<ButtonTooltip
asChild
type="default"
className="px-1.5"
tooltip={{ content: { side: 'bottom', text: 'View branch on GitHub' } }}
>
<a
target="_blank"
rel="noreferrer noopener"
href={`https://github.com/${repo}/tree/${branch.git_branch}`}
>
<Github size={14} className="text-foreground-light" />
</a>
</ButtonTooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center cursor-pointer" onClick={handleRowClick}>
{label || branch.name}
</div>
</TooltipTrigger>
{((page === 'branches' && !branch.is_default) || page === 'merge-requests') && (
<TooltipContent side="bottom">
{page === 'branches' && !branch.is_default && 'Switch to branch'}
{page === 'merge-requests' && 'View merge request'}
</TooltipContent>
)}
</Tooltip>
</div>
<div className="flex items-center gap-x-4">
<ButtonTooltip
asChild
type="default"
className="max-w-[300px]"
icon={
isMain ? (
<Shield strokeWidth={2} className="text-amber-900" />
) : branch.persistent ? (
<Infinity size={16} />
) : null
}
tooltip={{
content: {
side: 'bottom',
text: branch.persistent
? `${branch.name} is a persistent branch and will remain active even after the
underlying PR is closed`
: 'Switch to branch',
},
}}
>
<Link href={`/project/${branch.project_ref}`} title={branch.name}>
{branch.name}
</Link>
</ButtonTooltip>
{branch.git_branch && <Github size={14} className="text-foreground-light" />}
{isActive && <Badge>Current</Badge>}
<BranchStatusBadge
status={
branch.status === 'CREATING_PROJECT' ? data?.status ?? branch.status : branch.status
}
/>
<p className="text-xs text-foreground-lighter">
{daysFromNow > 1 ? `Updated on ${formattedUpdatedAt}` : `Updated ${formattedTimeFromNow}`}
</p>
<WorkflowLogs projectRef={branch.project_ref} status={branch.status} />
{rowActions}
</div>
<div className="flex items-center gap-x-8">
{isMain ? (
<div className="flex items-center gap-x-2">
{repo && (
<Button asChild type="default" iconRight={<ExternalLink size={14} />}>
<Link target="_blank" rel="noreferrer" passHref href={`https://github.com/${repo}`}>
View Repository
</Link>
</Button>
)}
<WorkflowLogs projectRef={branch.project_ref} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" icon={<MoreVertical />} className="px-1" />
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0 w-56" side="bottom" align="end">
{repo ? (
<Link passHref href={`/project/${projectRef}/settings/integrations`}>
<DropdownMenuItem asChild className="gap-x-2">
<a>Change production branch</a>
</DropdownMenuItem>
</Link>
) : (
<Tooltip>
<TooltipTrigger
asChild={canUpdateBranches && isBranchActiveHealthy}
className="w-full"
>
<DropdownMenuItem
className="gap-x-2"
disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdating}
onSelect={() => setShowEditBranchModal(true)}
onClick={() => setShowEditBranchModal(true)}
>
<Pencil size={14} />
Edit Branch
</DropdownMenuItem>
</TooltipTrigger>
{(!canUpdateBranches || !isBranchActiveHealthy) && (
<TooltipContent side="left">
{!canUpdateBranches
? 'You need additional permissions to edit branches'
: 'Branch is still initializing. Please wait for the branch to become healthy before editing.'}
</TooltipContent>
)}
</Tooltip>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
) : (
<div className="flex items-center gap-x-2">
{branch.git_branch && branch.pr_number === undefined ? (
<Button asChild type="default" iconRight={<ExternalLink size={14} />}>
<Link passHref target="_blank" rel="noreferrer" href={createPullRequestURL}>
Create Pull Request
</Link>
</Button>
) : branch.pr_number !== undefined ? (
<div className="flex items-center">
<Link
href={`https://github.com/${repo}/pull/${branch.pr_number}`}
target="_blank"
rel="noreferrer"
className="flex items-center gap-x-2 transition px-3 py-1 rounded-full bg-background-surface-400 hover:text-foreground"
>
<GitPullRequest size={14} />#{branch.pr_number}
</Link>
<ArrowRight className="mx-1 text-foreground-light" strokeWidth={1.5} size={16} />
<Button asChild type="default">
<Link
passHref
target="_blank"
rel="noreferer"
href={`http://github.com/${repo}/tree/${branch.git_branch}`}
>
{branch.git_branch}
</Link>
</Button>
</div>
) : null}
<WorkflowLogs projectRef={branch.project_ref} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" icon={<MoreVertical />} className="px-1" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" side="bottom" align="end">
<Tooltip>
<TooltipTrigger asChild={isBranchActiveHealthy} className="w-full">
<DropdownMenuItem
className="gap-x-2"
onSelect={() => setShowConfirmResetModal(true)}
onClick={() => setShowConfirmResetModal(true)}
disabled={isResetting || !isBranchActiveHealthy}
>
<RefreshCw size={14} />
Reset Branch
</DropdownMenuItem>
</TooltipTrigger>
{!isBranchActiveHealthy && (
<TooltipContent side="left">
Branch is still initializing. Please wait for the branch to become healthy
before resetting
</TooltipContent>
)}
</Tooltip>
<Tooltip>
<TooltipTrigger asChild={isBranchActiveHealthy} className="w-full">
<DropdownMenuItem
className="gap-x-2"
onSelect={() => setShowBranchModeSwitch(true)}
onClick={() => setShowBranchModeSwitch(true)}
disabled={!isBranchActiveHealthy}
>
{branch.persistent ? (
<>
<Clock size={14} /> Switch to ephemeral
</>
) : (
<>
<Infinity size={14} className="scale-110" /> Switch to persistent
</>
)}
</DropdownMenuItem>
</TooltipTrigger>
{!isBranchActiveHealthy && (
<TooltipContent side="left">
Branch is still initializing. Please wait for the branch to become healthy
before switching modes
</TooltipContent>
)}
</Tooltip>
{gitlessBranching && (
<Tooltip>
<TooltipTrigger
asChild={canUpdateBranches && isBranchActiveHealthy}
className="w-full"
>
<DropdownMenuItem
className="gap-x-2"
disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdating}
onSelect={() => setShowEditBranchModal(true)}
onClick={() => setShowEditBranchModal(true)}
>
<Pencil size={14} />
Edit Branch
</DropdownMenuItem>
</TooltipTrigger>
{(!canUpdateBranches || !isBranchActiveHealthy) && (
<TooltipContent side="left">
{!canUpdateBranches
? 'You need additional permissions to edit branches'
: 'Branch is still initializing. Please wait for the branch to become healthy before editing.'}
</TooltipContent>
)}
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild={canDeleteBranches} className="w-full">
<DropdownMenuItem
className="gap-x-2"
disabled={!canDeleteBranches}
onSelect={() => onSelectDeleteBranch?.()}
onClick={() => onSelectDeleteBranch?.()}
>
<Trash2 size={14} />
Delete branch
</DropdownMenuItem>
</TooltipTrigger>
{!canDeleteBranches && (
<TooltipContent side="left">
You need additional permissions to delete branches
</TooltipContent>
)}
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
<ConfirmationModal
variant={'destructive'}
visible={showConfirmResetModal}
confirmLabel="Reset branch"
title="Confirm branch reset"
loading={isResetting}
onCancel={() => {
setShowConfirmResetModal(false)
}}
onConfirm={onConfirmReset}
>
<p className="text-sm text-foreground-light">
Are you sure you want to reset the "{branch.name}" branch? All data will be deleted.
</p>
</ConfirmationModal>
<ConfirmationModal
variant={'default'}
visible={showBranchModeSwitch}
confirmLabel={branch.persistent ? 'Switch to ephemeral' : 'Switch to persistent'}
title={`Confirm branch mode switch`}
loading={isUpdating}
onCancel={() => {
setShowBranchModeSwitch(false)
}}
onConfirm={onUpdateBranchPersistentMode}
>
<p className="text-sm text-foreground-light">
Are you sure you want to switch the branch "{branch.name}" to{' '}
{branch.persistent ? 'ephemeral' : 'persistent'} mode?
<br />
<br />
{branch.persistent
? 'Ephemeral branches will be deleted once the underlying PR closes.'
: 'Persistent branches will remain active even after the underlying PR is closed.'}
</p>
</ConfirmationModal>
</div>
)}
</div>
<EditBranchModal
branch={branch}
visible={showEditBranchModal}
onClose={() => setShowEditBranchModal(false)}
/>
</div>
)
}
@@ -1,9 +1,9 @@
import { Check, Plus, Shield } from 'lucide-react'
import { Check, GitMerge, Shield } from 'lucide-react'
import { useState } from 'react'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { Branch } from 'data/branches/branches-query'
import {
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
@@ -21,6 +21,7 @@ interface BranchSelectorProps {
onBranchSelected?: (branch: Branch) => void
disabled?: boolean
isUpdating?: boolean
type?: 'primary' | 'outline'
}
export const BranchSelector = ({
@@ -28,6 +29,7 @@ export const BranchSelector = ({
onBranchSelected,
disabled = false,
isUpdating = false,
type = 'primary',
}: BranchSelectorProps) => {
const [open, setOpen] = useState(false)
const [selectedBranch, setSelectedBranch] = useState<Branch | null>(null)
@@ -46,16 +48,26 @@ export const BranchSelector = ({
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="outline"
size="tiny"
<ButtonTooltip
icon={<GitMerge size={14} strokeWidth={1.5} />}
type={type}
disabled={disabled || availableBranches.length === 0 || isUpdating}
icon={<Plus size={14} strokeWidth={1.5} />}
tooltip={{
content: {
side: 'bottom',
text:
branches.length === 0
? 'Create a branch first to start a merge request'
: availableBranches.length === 0
? 'All branches currently have merge requests'
: undefined,
},
}}
>
{isUpdating ? 'Creating...' : 'Create review'}
</Button>
{isUpdating ? 'Creating...' : 'New merge request'}
</ButtonTooltip>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="p-0 w-80" side="bottom">
<PopoverContent_Shadcn_ portal className="p-0 w-80" side="bottom" align="end">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find branch to review..." />
<CommandList_Shadcn_>
@@ -1,16 +1,19 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import { DollarSign, Github, GitMerge, Loader2, Merge } from 'lucide-react'
import { DollarSign, GitMerge, Github, Loader2 } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { BranchingPITRNotice } from 'components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
@@ -21,7 +24,6 @@ import { projectKeys } from 'data/projects/keys'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useFlag } from 'hooks/ui/useFlag'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import {
@@ -42,7 +44,8 @@ import {
cn,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useRouter } from 'next/router'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { PermissionAction } from '@supabase/shared-types/out/constants'
export const CreateBranchModal = () => {
const { ref } = useParams()
@@ -50,7 +53,7 @@ export const CreateBranchModal = () => {
const queryClient = useQueryClient()
const projectDetails = useSelectedProject()
const selectedOrg = useSelectedOrganization()
const gitlessBranching = useFlag('gitlessBranching')
const gitlessBranching = useIsBranching2Enabled()
const { showCreateBranchModal, setShowCreateBranchModal } = useAppStateSnapshot()
const organization = useSelectedOrganization()
@@ -94,6 +97,8 @@ export const CreateBranchModal = () => {
},
})
const canCreateBranch = useCheckPermissions(PermissionAction.CREATE, 'preview_branches')
const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
// Fetch production/default branch to inspect git_branch linkage
@@ -148,6 +153,14 @@ export const CreateBranchModal = () => {
})
const canSubmit = !isCreating && !isChecking
const isDisabled =
!isSuccessConnections ||
isCreating ||
!canSubmit ||
isChecking ||
(!gitlessBranching && !githubConnection) ||
promptProPlanUpgrade ||
!canCreateBranch
const onSubmit = (data: z.infer<typeof FormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
@@ -197,6 +210,7 @@ export const CreateBranchModal = () => {
<DialogSectionSeparator />
</>
)}
<DialogSection
padding="medium"
className={cn('space-y-4', promptProPlanUpgrade && 'opacity-25 pointer-events-none')}
@@ -347,9 +361,7 @@ export const CreateBranchModal = () => {
</figure>
</div>
<div className="flex flex-col gap-y-1">
<p className="text-sm text-foreground">
Preview branches are billed $0.01344 per hour
</p>
<p className="text-sm text-foreground">Branches are billed $0.01344 per hour</p>
<p className="text-sm text-foreground-light">
This cost will continue for as long as the branch has not been removed.
</p>
@@ -367,22 +379,24 @@ export const CreateBranchModal = () => {
>
Cancel
</Button>
<Button
<ButtonTooltip
form={formId}
disabled={
!isSuccessConnections ||
isCreating ||
!canSubmit ||
isChecking ||
(!gitlessBranching && !githubConnection) ||
promptProPlanUpgrade
}
disabled={isDisabled}
loading={isCreating}
type="primary"
htmlType="submit"
tooltip={{
content: {
side: 'bottom',
text:
!gitlessBranching && !githubConnection
? 'Set up a GitHub connection first to create branches'
: undefined,
},
}}
>
Create branch
</Button>
</ButtonTooltip>
</DialogFooter>
</form>
</Form_Shadcn_>
@@ -1,6 +1,7 @@
import Link from 'next/link'
import { CircleAlert, Database, Wind } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from 'ui'
import { CircleAlert, Database, Download, Wind } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle, Skeleton, Button } from 'ui'
import { toast } from 'sonner'
import DiffViewer from 'components/ui/DiffViewer'
@@ -45,7 +46,7 @@ const DatabaseDiffPanel = ({
return (
<Card>
<CardHeader>
<CardHeader className="flex flex-row items-center justify-between space-y-0 py-3">
<CardTitle>
<Link
href={`/project/${currentBranchRef}/database/schema`}
@@ -55,6 +56,37 @@ const DatabaseDiffPanel = ({
Schema Changes
</Link>
</CardTitle>
<Button
type="default"
size="tiny"
icon={<Download strokeWidth={1.5} size={14} className="text-foreground-light" />}
className="mt-0"
onClick={() => {
if (!diffContent) return
const now = new Date()
const pad = (n: number) => n.toString().padStart(2, '0')
const timestamp =
now.getFullYear().toString() +
pad(now.getMonth() + 1) +
pad(now.getDate()) +
pad(now.getHours()) +
pad(now.getMinutes()) +
pad(now.getSeconds())
const filename = `${timestamp}_migration.sql`
const blob = new Blob([diffContent], { type: 'text/plain;charset=utf-8;' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.setAttribute('download', filename)
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
toast.success('Migration file downloaded!')
}}
>
Download as migration
</Button>
</CardHeader>
<CardContent className="p-0 h-96">
<DiffViewer language="sql" original="" modified={diffContent} />
@@ -1,4 +1,4 @@
import { Code, Wind } from 'lucide-react'
import { Circle, Code, Minus, Plus, Wind } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
@@ -34,15 +34,31 @@ const fileKey = (fullPath: string) => basename(fullPath)
const getStatusColor = (status: FileStatus): string => {
switch (status) {
case 'added':
return 'bg-brand'
return 'text-brand'
case 'removed':
return 'bg-destructive'
return 'text-destructive'
case 'modified':
return 'bg-warning'
return 'text-warning'
case 'unchanged':
return 'bg-muted'
return 'text-muted'
default:
return 'bg-muted'
return 'text-muted'
}
}
// Helper to get the status icon for file indicators
const getStatusIcon = (status: FileStatus) => {
switch (status) {
case 'added':
return Plus
case 'removed':
return Minus
case 'modified':
return Circle
case 'unchanged':
return Circle
default:
return Circle
}
}
@@ -65,8 +81,12 @@ const FunctionDiff = ({
}
}, [allFileKeys, activeFileKey])
const currentFile = currentBody.find((f) => fileKey(f.name) === activeFileKey)
const mainFile = mainBody.find((f) => fileKey(f.name) === activeFileKey)
const currentFile = currentBody.find(
(f: EdgeFunctionBodyData[number]) => fileKey(f.name) === activeFileKey
)
const mainFile = mainBody.find(
(f: EdgeFunctionBodyData[number]) => fileKey(f.name) === activeFileKey
)
const language = useMemo(() => {
if (!activeFileKey) return 'plaintext'
@@ -96,28 +116,31 @@ const FunctionDiff = ({
<div className="flex h-full min-h-0">
<div className="w-48 border-r bg-surface-200 flex flex-col overflow-y-auto">
<ul className="divide-y divide-border">
{fileInfos.map((fileInfo) => (
<li key={fileInfo.key} className="flex">
<button
type="button"
onClick={() => setActiveFileKey(fileInfo.key)}
className={cn(
'flex-1 text-left text-xs px-4 py-2 flex items-center gap-2',
activeFileKey === fileInfo.key
? 'bg-surface-300 text-foreground'
: 'text-foreground-light hover:bg-surface-300'
)}
>
<div
{fileInfos.map((fileInfo) => {
const Icon = getStatusIcon(fileInfo.status)
return (
<li key={fileInfo.key} className="flex">
<button
type="button"
onClick={() => setActiveFileKey(fileInfo.key)}
className={cn(
'w-1 h-1 rounded-full flex-shrink-0',
getStatusColor(fileInfo.status)
'flex-1 text-left text-xs px-4 py-2 flex items-center gap-2',
activeFileKey === fileInfo.key
? 'bg-surface-300 text-foreground'
: 'text-foreground-light hover:bg-surface-300'
)}
/>
<span className="truncate">{fileInfo.key}</span>
</button>
</li>
))}
>
<Icon
className={cn('flex-shrink-0', getStatusColor(fileInfo.status))}
size={12}
strokeWidth={1}
/>
<span className="truncate">{fileInfo.key}</span>
</button>
</li>
)
})}
</ul>
</div>
<div className="flex-1 min-h-0">
@@ -142,7 +165,9 @@ const EdgeFunctionsDiffPanel = ({
return <Skeleton className="h-64" />
}
if (!diffResults.hasChanges) {
const noChanges = diffResults.addedSlugs.length === 0 && diffResults.modifiedSlugs.length === 0
if (noChanges) {
return (
<div className="p-6 text-center">
<Wind size={32} strokeWidth={1.5} className="text-foreground-muted mx-auto mb-8" />
@@ -16,7 +16,7 @@ import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-ch
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useFlag } from 'hooks/ui/useFlag'
import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { BASE_PATH } from 'lib/constants'
import { useRouter } from 'next/router'
import {
@@ -50,7 +50,7 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
const router = useRouter()
const projectDetails = useSelectedProject()
const selectedOrg = useSelectedOrganization()
const gitlessBranching = useFlag('gitlessBranching')
const gitlessBranching = useIsBranching2Enabled()
const [isGitBranchValid, setIsGitBranchValid] = useState(false)
@@ -219,7 +219,7 @@ export const EditBranchModal = ({ branch, visible, onClose }: EditBranchModalPro
control={form.control}
name="branchName"
render={({ field }) => (
<FormItemLayout label="Preview Branch Name">
<FormItemLayout label="Preview branch name">
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
@@ -1,125 +1,57 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ExternalLink, GitBranch, GitPullRequest, Github } from 'lucide-react'
import type { Branch } from 'data/branches/branches-query'
import { Github } from 'lucide-react'
import Link from 'next/link'
import { BranchSelector } from './BranchSelector'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAppStateSnapshot } from 'state/app-state'
import { sidePanelsState } from 'state/side-panels'
import { DocsButton } from 'components/ui/DocsButton'
import { Button } from 'ui'
export const BranchingEmptyState = () => {
const snap = useAppStateSnapshot()
const canCreateBranches = useCheckPermissions(PermissionAction.CREATE, 'preview_branches', {
resource: { is_default: false },
})
return (
<ProductEmptyState title="Database Branching">
<p className="text-sm text-light">
Create preview branches to experiment with changes to your database schema in a safe,
non-destructive environment.
</p>
<div className="flex items-center space-x-2 !mt-4">
<ButtonTooltip
disabled={!canCreateBranches}
icon={<GitBranch strokeWidth={1.5} />}
onClick={() => snap.setShowCreateBranchModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canCreateBranches
? 'You need additional permissions to create branches'
: undefined,
},
}}
>
Create branch
</ButtonTooltip>
<Button type="default" icon={<ExternalLink strokeWidth={1.5} />} asChild>
<a
target="_blank"
rel="noreferrer"
href="https://supabase.com/docs/guides/platform/branching"
>
View the docs
</a>
</Button>
</div>
</ProductEmptyState>
)
}
const EMPTY_STATE_CONTAINER = 'flex items-center flex-col justify-center w-full py-10 px-4'
export const PullRequestsEmptyState = ({
url,
projectRef,
hasBranches,
branches,
onBranchSelected,
isUpdating,
githubConnection,
gitlessBranching,
gitlessBranching = false,
}: {
url: string
projectRef: string
hasBranches: boolean
branches: Branch[]
onBranchSelected: (branch: Branch) => void
isUpdating: boolean
githubConnection?: any
gitlessBranching?: boolean
gitlessBranching: boolean
}) => {
// Show GitHub connection message if GitHub is not connected and gitless branching is enabled
if (!githubConnection && gitlessBranching) {
return (
<div className="p-16 text-center">
<div>
<h3 className="mb-1">Connect to GitHub for seamless branching</h3>
<p className="text-sm text-foreground-light mb-4">
Sync GitHub repos to Supabase projects for automatic branch creation and merging
</p>
</div>
<Button type="default" icon={<Github />} asChild>
<Link href={`/project/${projectRef}/settings/integrations`}>Connect to GitHub</Link>
</Button>
</div>
)
}
return (
<div className="flex items-center flex-col justify-center w-full py-10">
<p>No pull requests made yet for this repository</p>
<p className="text-foreground-light">
Only pull requests with the ./migration directory changes will show here.
<div className={EMPTY_STATE_CONTAINER}>
<p>No merge requests</p>
<p className="text-foreground-light text-center">
Create your first merge request to merge changes back to the main branch
</p>
{hasBranches && (
<div className="w-96 border rounded-md mt-4">
<div className="px-5 py-3 bg-surface-100 flex items-center justify-between">
<div className="flex items-center space-x-4">
<GitPullRequest size={18} strokeWidth={2} className="text-foreground-light" />
<p>Create a pull request</p>
</div>
<Button asChild type="default" iconRight={<ExternalLink size={14} />}>
<Link passHref target="_blank" rel="noreferrer" href={url}>
Github
</Link>
</Button>
</div>
<div className="px-5 py-3 border-t flex items-center justify-between">
<div>
<p>Not sure what to do?</p>
<p className="text-foreground-light">Browse our documentation</p>
</div>
<Button type="default" iconRight={<ExternalLink size={14} />}>
<Link
target="_blank"
rel="noreferrer"
href="https://supabase.com/docs/guides/platform/branching"
>
Docs
</Link>
</Button>
</div>
</div>
)}
<div className="flex items-center space-x-2 mt-4">
{githubConnection ? (
<Button type="outline" asChild icon={<Github />}>
<a href={url} target="_blank" rel="noopener noreferrer">
Create pull request
</a>
</Button>
) : (
<Button asChild type="outline">
<Link href={`/project/${projectRef}/settings/integrations`}>Connect to GitHub</Link>
</Button>
)}
{gitlessBranching && (
<BranchSelector
type="outline"
branches={branches}
onBranchSelected={onBranchSelected}
isUpdating={isUpdating}
/>
)}
</div>
</div>
)
}
@@ -130,22 +62,14 @@ export const PreviewBranchesEmptyState = ({
onSelectCreateBranch: () => void
}) => {
return (
<div className="flex items-center flex-col justify-center w-full py-10">
<div className={EMPTY_STATE_CONTAINER}>
<p>Create your first preview branch</p>
<p className="text-foreground-light mb-4">
Preview branches are used to experiment with changes to your database schema in a safe,
non-destructive environment.
<p className="text-foreground-light text-center mb-4">
Preview branches are short-lived environments that let you safely experiment with changes to
your database schema without affecting your main database.
</p>
<div className="flex items-center space-x-2">
<Button type="default" iconRight={<ExternalLink size={14} />}>
<Link
target="_blank"
rel="noreferrer"
href="https://supabase.com/docs/guides/platform/branching"
>
Docs
</Link>
</Button>
<DocsButton href="https://supabase.com/docs/guides/platform/branching" />
<Button type="primary" onClick={() => onSelectCreateBranch()}>
Create your first branch
</Button>
@@ -1,24 +1,51 @@
import { noop } from 'lodash'
import { Button } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { partition } from 'lodash'
import {
Clock,
ExternalLink,
Infinity,
MoreVertical,
Pencil,
RefreshCw,
Shield,
Trash2,
} from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import { useBranchQuery } from 'data/branches/branch-query'
import { useBranchResetMutation } from 'data/branches/branch-reset-mutation'
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
import type { Branch } from 'data/branches/branches-query'
import { ChevronRight } from 'lucide-react'
import { branchKeys } from 'data/branches/keys'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels'
import { EditBranchModal } from './EditBranchModal'
import { PreviewBranchesEmptyState } from './EmptyStates'
const MAX_BRANCHES_OVERVIEW = 10
interface OverviewProps {
isLoading: boolean
isSuccess: boolean
repo: string
mainBranch: Branch
previewBranches: Branch[]
onViewAllBranches: () => void
onSelectCreateBranch: () => void
onSelectDeleteBranch: (branch: Branch) => void
generateCreatePullRequestURL: (branchName?: string) => string
showProductionBranch?: boolean
}
export const Overview = ({
@@ -27,48 +54,98 @@ export const Overview = ({
repo,
mainBranch,
previewBranches,
onViewAllBranches,
onSelectCreateBranch,
onSelectDeleteBranch,
generateCreatePullRequestURL,
showProductionBranch = true,
}: OverviewProps) => {
const [persistentBranches, ephemeralBranches] = partition(
previewBranches,
(branch) => branch.persistent
)
const { ref: projectRef } = useParams()
return (
<>
{showProductionBranch && (
<BranchManagementSection header="Production branch">
{isLoading && <BranchRowLoader />}
{isSuccess && mainBranch !== undefined && (
<BranchRow isMain branch={mainBranch} repo={repo} onSelectDeleteBranch={noop} />
)}
</BranchManagementSection>
)}
<BranchManagementSection header="Production branch">
{isLoading && <BranchRowLoader />}
{isSuccess && mainBranch !== undefined && (
<BranchRow
branch={mainBranch}
label={
<div className="flex items-center gap-x-2">
<Shield size={14} strokeWidth={1.5} className="text-warning" />
{mainBranch.name}
</div>
}
repo={repo}
rowActions={<MainBranchActions branch={mainBranch} repo={repo} />}
/>
)}
{isSuccess && mainBranch === undefined && (
<div className="w-full flex items-center justify-between px-4 py-2.5 hover:bg-surface-100">
<Link href={`/project/${projectRef}`} className="text-foreground block w-full">
<div className="flex items-center gap-x-3">
<Shield size={14} strokeWidth={1.5} className="text-warning" />
main
</div>
</Link>
</div>
)}
</BranchManagementSection>
<BranchManagementSection
header="Preview branches"
footer={
isSuccess && (
<div className="flex items-center justify-center">
<Button type="text" iconRight={<ChevronRight />} onClick={() => onViewAllBranches()}>
View all branches
</Button>
</div>
)
}
>
{/* Persistent Branches Section */}
<BranchManagementSection header="Persistent branches">
{isLoading && <BranchLoader />}
{isSuccess && previewBranches.length === 0 && (
<PreviewBranchesEmptyState onSelectCreateBranch={onSelectCreateBranch} />
{isSuccess && persistentBranches.length === 0 && (
<div className="flex items-center flex-col justify-center w-full py-10">
<p>No persistent branches</p>
<p className="text-foreground-light text-center">
Persistent branches are long-lived, cannot be reset, and are ideal for staging
environments.
</p>
</div>
)}
{isSuccess &&
previewBranches.slice(0, MAX_BRANCHES_OVERVIEW).map((branch) => {
persistentBranches.map((branch) => {
return (
<BranchRow
key={branch.id}
repo={repo}
branch={branch}
generateCreatePullRequestURL={generateCreatePullRequestURL}
onSelectDeleteBranch={() => onSelectDeleteBranch(branch)}
rowActions={
<PreviewBranchActions
branch={branch}
repo={repo}
onSelectDeleteBranch={() => onSelectDeleteBranch(branch)}
generateCreatePullRequestURL={generateCreatePullRequestURL}
/>
}
/>
)
})}
</BranchManagementSection>
{/* Ephemeral/Preview Branches Section */}
<BranchManagementSection header="Preview branches">
{isLoading && <BranchLoader />}
{isSuccess && ephemeralBranches.length === 0 && (
<PreviewBranchesEmptyState onSelectCreateBranch={onSelectCreateBranch} />
)}
{isSuccess &&
ephemeralBranches.map((branch) => {
return (
<BranchRow
key={branch.id}
repo={repo}
branch={branch}
rowActions={
<PreviewBranchActions
branch={branch}
repo={repo}
onSelectDeleteBranch={() => onSelectDeleteBranch(branch)}
generateCreatePullRequestURL={generateCreatePullRequestURL}
/>
}
/>
)
})}
@@ -76,3 +153,268 @@ export const Overview = ({
</>
)
}
// Row actions for preview branches (non-main)
const PreviewBranchActions = ({
branch,
onSelectDeleteBranch,
generateCreatePullRequestURL,
}: {
branch: Branch
repo: string
onSelectDeleteBranch: () => void
generateCreatePullRequestURL: (branchName?: string) => string
}) => {
const gitlessBranching = useIsBranching2Enabled()
const queryClient = useQueryClient()
const projectRef = branch.parent_project_ref ?? branch.project_ref
const canDeleteBranches = useCheckPermissions(PermissionAction.DELETE, 'preview_branches')
const canUpdateBranches = useCheckPermissions(PermissionAction.UPDATE, 'preview_branches')
const { data } = useBranchQuery({ projectRef, id: branch.id })
const isBranchActiveHealthy = data?.status === 'ACTIVE_HEALTHY'
const [showConfirmResetModal, setShowConfirmResetModal] = useState(false)
const [showBranchModeSwitch, setShowBranchModeSwitch] = useState(false)
const [showEditBranchModal, setShowEditBranchModal] = useState(false)
const { mutate: resetBranch, isLoading: isResetting } = useBranchResetMutation({
onSuccess() {
toast.success('Success! Please allow a few seconds for the branch to reset.')
setShowConfirmResetModal(false)
},
})
const { mutate: updateBranch, isLoading: isUpdatingBranch } = useBranchUpdateMutation({
onSuccess() {
toast.success('Successfully updated branch')
setShowBranchModeSwitch(false)
if (projectRef) {
queryClient.invalidateQueries({ queryKey: branchKeys.list(projectRef) })
}
},
})
const onConfirmReset = () => {
if (!projectRef) return
resetBranch({ id: branch.id, projectRef })
}
const onTogglePersistent = () => {
if (!projectRef) return
updateBranch({ id: branch.id, projectRef, persistent: !branch.persistent })
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="text"
icon={<MoreVertical />}
className="px-1"
onClick={(e) => e.stopPropagation()}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" side="bottom" align="end">
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={isResetting || !isBranchActiveHealthy}
onSelect={(e) => {
e.stopPropagation()
setShowConfirmResetModal(true)
}}
onClick={(e) => {
e.stopPropagation()
setShowConfirmResetModal(true)
}}
tooltip={{
content: {
side: 'left',
text: !isBranchActiveHealthy
? 'Branch is still initializing. Please wait for it to become healthy before resetting.'
: undefined,
},
}}
>
<RefreshCw size={14} /> Reset branch
</DropdownMenuItemTooltip>
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!isBranchActiveHealthy}
onSelect={(e) => {
e.stopPropagation()
setShowBranchModeSwitch(true)
}}
onClick={(e) => {
e.stopPropagation()
setShowBranchModeSwitch(true)
}}
tooltip={{
content: {
side: 'left',
text: !isBranchActiveHealthy
? 'Branch is still initializing. Please wait for it to become healthy before switching.'
: undefined,
},
}}
>
{branch.persistent ? (
<>
<Clock size={14} /> Switch to preview
</>
) : (
<>
<Infinity size={14} className="scale-110" /> Switch to persistent
</>
)}
</DropdownMenuItemTooltip>
{/* Edit Branch (gitless) */}
{gitlessBranching && (
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canUpdateBranches || !isBranchActiveHealthy || isUpdatingBranch}
onSelect={(e) => {
e.stopPropagation()
setShowEditBranchModal(true)
}}
onClick={(e) => {
e.stopPropagation()
setShowEditBranchModal(true)
}}
tooltip={{
content: {
side: 'left',
text: !canUpdateBranches
? 'You need additional permissions to edit branches'
: !isBranchActiveHealthy
? 'Branch is still initializing. Please wait for it to become healthy before editing.'
: undefined,
},
}}
>
<Pencil size={14} /> Edit branch
</DropdownMenuItemTooltip>
)}
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canDeleteBranches}
onSelect={(e) => {
e.stopPropagation()
onSelectDeleteBranch()
}}
onClick={(e) => {
e.stopPropagation()
onSelectDeleteBranch()
}}
tooltip={{
content: {
side: 'left',
text: !canDeleteBranches
? 'You need additional permissions to delete branches'
: undefined,
},
}}
>
<Trash2 size={14} /> Delete branch
</DropdownMenuItemTooltip>
{/* Create PR if applicable */}
{branch.git_branch && branch.pr_number === undefined && (
<DropdownMenuItem asChild className="gap-x-2">
<a
target="_blank"
rel="noreferrer"
href={generateCreatePullRequestURL(branch.git_branch)}
onClick={(e) => e.stopPropagation()}
>
<ExternalLink size={14} /> Create pull request
</a>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<TextConfirmModal
variant="warning"
visible={showConfirmResetModal}
onCancel={() => setShowConfirmResetModal(false)}
onConfirm={onConfirmReset}
loading={isResetting}
title="Reset branch"
confirmLabel="Reset branch"
confirmPlaceholder="Type in name of branch"
confirmString={branch?.name ?? ''}
alert={{
title: `Are you sure you want to reset the "${branch.name}" branch? All data will be deleted.`,
}}
/>
<ConfirmationModal
variant="default"
visible={showBranchModeSwitch}
confirmLabel={branch.persistent ? 'Switch to preview' : 'Switch to persistent'}
title="Confirm branch mode switch"
loading={isUpdatingBranch}
onCancel={() => setShowBranchModeSwitch(false)}
onConfirm={onTogglePersistent}
>
<p className="text-sm text-foreground-light">
Are you sure you want to switch the branch "{branch.name}" to{' '}
{branch.persistent ? 'preview' : 'persistent'}?
</p>
</ConfirmationModal>
<EditBranchModal
branch={branch}
visible={showEditBranchModal}
onClose={() => setShowEditBranchModal(false)}
/>
</>
)
}
// Actions for main (production) branch
const MainBranchActions = ({ branch, repo }: { branch: Branch; repo: string }) => {
const { ref: projectRef } = useParams()
const canUpdateBranches = useCheckPermissions(PermissionAction.UPDATE, 'preview_branches')
const [showEditBranchModal, setShowEditBranchModal] = useState(false)
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" icon={<MoreVertical />} className="px-1" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" side="bottom" align="end">
{repo ? (
<Link passHref href={`/project/${projectRef}/settings/integrations`}>
<DropdownMenuItem asChild className="gap-x-2">
<a>Change production branch</a>
</DropdownMenuItem>
</Link>
) : (
<DropdownMenuItem
className="gap-x-2"
disabled={!canUpdateBranches}
onSelect={() => setShowEditBranchModal(true)}
onClick={() => setShowEditBranchModal(true)}
>
<Pencil size={14} /> Edit Branch
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<EditBranchModal
branch={branch}
visible={showEditBranchModal}
onClose={() => setShowEditBranchModal(false)}
/>
</>
)
}
@@ -49,7 +49,7 @@ export const ReviewWithAI = ({
if (productionSQL.trim()) {
sqlSnippets.push({
label: 'Production Schema',
content: productionSQL,
content: 'CURRENT PRODUCTION SCHEMA:\n' + productionSQL,
})
}
}
@@ -58,7 +58,7 @@ export const ReviewWithAI = ({
if (diffContent && diffContent.trim()) {
sqlSnippets.push({
label: 'Database Changes',
content: '-- DATABASE CHANGES:\n' + diffContent,
content: '-- DATABASE CHANGES TO BE MERGED IN:\n' + diffContent,
})
}
@@ -66,18 +66,7 @@ export const ReviewWithAI = ({
name: `Review merge: ${currentBranch.name} → ${mainBranch.name}`,
open: true,
sqlSnippets: sqlSnippets.length > 0 ? sqlSnippets : undefined,
initialInput: `Please review this merge request from branch "${currentBranch.name}" into "${mainBranch.name || 'main'}".
I've included the current production schema as context, along with the proposed database changes.
Analyze the changes and provide feedback on:
- Database schema changes and potential impacts on the production schema
- Migration safety and rollback considerations
- Overall code quality and best practices
- Potential breaking changes or compatibility issues
- Data integrity and constraint implications
Please be concise with your response.`,
initialInput: `I want to run the attached database changes on my production database branch as part of a branch merge from "${currentBranch.name}" into "${mainBranch.name || 'main'}". I've included the current production database schema as extra context. Please analyze the proposed schema changes and provide concise feedback on their impact on the production schema including any migration concerns and potential conflicts.`,
suggestions: {
title: `I can help you review the database schema changes from "${currentBranch.name}" to "${mainBranch.name}", here are some specific areas I can focus on:`,
prompts: [
@@ -1,6 +1,8 @@
import type { Branch } from 'data/branches/branches-query'
import dayjs from 'dayjs'
import { ArrowLeft, ArrowRight } from 'lucide-react'
import { useState } from 'react'
import { StatusIcon } from 'ui'
import AlertError from 'components/ui/AlertError'
import { useWorkflowRunLogsQuery } from 'data/workflow-runs/workflow-run-logs-query'
@@ -22,9 +24,20 @@ import BranchStatusBadge from './BranchStatusBadge'
interface WorkflowLogsProps {
projectRef: string
status?: Branch['status'] | string
}
const WorkflowLogs = ({ projectRef }: WorkflowLogsProps) => {
type StatusType = Branch['status'] | string
const UNHEALTHY_STATUSES: StatusType[] = [
'ACTIVE_UNHEALTHY',
'INIT_FAILED',
'UNKNOWN',
'MIGRATIONS_FAILED',
'FUNCTIONS_FAILED',
]
export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
const [isOpen, setIsOpen] = useState(false)
const {
@@ -33,14 +46,7 @@ const WorkflowLogs = ({ projectRef }: WorkflowLogsProps) => {
isLoading: isWorkflowRunsLoading,
isError: isWorkflowRunsError,
error: workflowRunsError,
} = useWorkflowRunsQuery(
{
projectRef,
},
{
enabled: isOpen,
}
)
} = useWorkflowRunsQuery({ projectRef }, { enabled: isOpen })
const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState<string | undefined>(undefined)
@@ -51,24 +57,36 @@ const WorkflowLogs = ({ projectRef }: WorkflowLogsProps) => {
isError: isWorkflowRunLogsError,
error: workflowRunLogsError,
} = useWorkflowRunLogsQuery(
{
workflowRunId: selectedWorkflowRunId,
},
{
enabled: isOpen && selectedWorkflowRunId !== undefined,
}
{ workflowRunId: selectedWorkflowRunId },
{ enabled: isOpen && selectedWorkflowRunId !== undefined }
)
const showStatusIcon =
status !== undefined &&
status !== 'ACTIVE_HEALTHY' &&
status !== 'FUNCTIONS_DEPLOYED' &&
status !== 'MIGRATIONS_PASSED'
const isUnhealthy = status !== undefined && UNHEALTHY_STATUSES.includes(status)
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button type="default">View Logs</Button>
<Button
type="default"
icon={
showStatusIcon ? (
<StatusIcon variant={isUnhealthy ? 'destructive' : 'default'} hideBackground />
) : undefined
}
onClick={(e) => e.stopPropagation()}
>
View Logs
</Button>
</DialogTrigger>
<DialogContent size="xlarge">
<DialogHeader>
<DialogTitle>Workflow Logs</DialogTitle>
<DialogDescription>Select a workflow run to view logs</DialogDescription>
</DialogHeader>
@@ -143,5 +161,3 @@ const WorkflowLogs = ({ projectRef }: WorkflowLogsProps) => {
</Dialog>
)
}
export default WorkflowLogs
@@ -48,7 +48,7 @@ export const TimezoneSelection = ({
: 'Select timezone...'}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="w-[350px] p-0">
<PopoverContent_Shadcn_ portal className="w-[350px] p-0">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search timezone..." className="h-9" />
<CommandList_Shadcn_>
@@ -4,9 +4,12 @@ import { ReplicationPipelinesData } from 'data/replication/pipelines-query'
import { ResponseError } from 'types'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import RowMenu from './RowMenu'
import PipelineStatus, { PipelineStatusRequestStatus } from './PipelineStatus'
import PipelineStatus, { PipelineStatusRequestStatus, PipelineStatusName } from './PipelineStatus'
import { useParams } from 'common'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import {
ReplicationPipelineStatusData,
useReplicationPipelineStatusQuery,
} from 'data/replication/pipeline-status-query'
import { useState } from 'react'
import { toast } from 'sonner'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
@@ -65,10 +68,22 @@ const DestinationRow = ({
const { mutateAsync: startPipeline } = useStartPipelineMutation()
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
const pipelineStatus = pipelineStatusData?.status
const getStatusName = (
status: ReplicationPipelineStatusData['status'] | undefined
): string | undefined => {
if (status && typeof status === 'object' && 'name' in status) {
return status.name
}
return undefined
}
const statusName = getStatusName(pipelineStatus)
if (
(requestStatus === PipelineStatusRequestStatus.EnableRequested &&
pipelineStatus === 'Started') ||
(requestStatus === PipelineStatusRequestStatus.DisableRequested && pipelineStatus === 'Stopped')
(statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED)) ||
(requestStatus === PipelineStatusRequestStatus.DisableRequested &&
(statusName === PipelineStatusName.STOPPED || statusName === PipelineStatusName.FAILED))
) {
setRequestStatus(PipelineStatusRequestStatus.None)
}
@@ -190,7 +205,7 @@ const DestinationRow = ({
sourceId,
destinationId: destinationId,
pipelineId: pipeline?.id,
enabled: pipelineStatusData?.status === 'Started',
enabled: statusName === PipelineStatusName.STARTED,
}}
/>
</>
@@ -2,8 +2,21 @@ import AlertError from 'components/ui/AlertError'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { cn } from 'ui'
import { ResponseError } from 'types'
import { Loader2 } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
import { Loader2, ChevronDown, ChevronRight, Copy, AlertTriangle } from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
Button,
Collapsible_Shadcn_ as Collapsible,
CollapsibleContent_Shadcn_ as CollapsibleContent,
CollapsibleTrigger_Shadcn_ as CollapsibleTrigger,
} from 'ui'
import { useState, useEffect, useRef } from 'react'
import { copyToClipboard } from 'ui'
import { toast } from 'sonner'
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
export enum PipelineStatusRequestStatus {
None = 'None',
@@ -11,8 +24,19 @@ export enum PipelineStatusRequestStatus {
DisableRequested = 'DisableRequested',
}
export enum PipelineStatusName {
FAILED = 'failed',
STARTING = 'starting',
STARTED = 'started',
STOPPED = 'stopped',
UNKNOWN = 'unknown',
}
// Type alias for better readability
type FailedStatus = Extract<ReplicationPipelineStatusData['status'], { name: 'failed' }>
interface PipelineStatusProps {
pipelineStatus: string | undefined
pipelineStatus: ReplicationPipelineStatusData['status'] | undefined
error: ResponseError | null
isLoading: boolean
isError: boolean
@@ -28,6 +52,175 @@ const PipelineStatus = ({
isSuccess,
requestStatus,
}: PipelineStatusProps) => {
const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false)
const errorDetailsRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (errorDetailsRef.current && !errorDetailsRef.current.contains(event.target as Node)) {
setIsErrorDetailsOpen(false)
}
}
if (isErrorDetailsOpen) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isErrorDetailsOpen])
const handleCopyToClipboard = async (text: string) => {
try {
await copyToClipboard(text)
toast.success('Error details copied to clipboard')
} catch (error) {
toast.error('Failed to copy error details')
}
}
const isFailedStatus = (
status: ReplicationPipelineStatusData['status'] | undefined
): status is FailedStatus => {
return (
status !== null &&
status !== undefined &&
typeof status === 'object' &&
status.name === PipelineStatusName.FAILED
)
}
const isLogLoadingState = (failedStatus: FailedStatus): boolean => {
// Right now we hardcode the error message which is returned when k8s is not able to find logs, which seems
// to be a transient error. In case we find a way to properly handle this in the backend, this hack will not
// be needed anymore.
return (
failedStatus.message?.startsWith('unable to retrieve container logs for containerd://') ===
true
)
}
const renderFailedStatus = (failedStatus: FailedStatus) => {
const hasDetails =
failedStatus.message || failedStatus.reason || failedStatus.exit_code !== undefined
const isLoadingLogs = isLogLoadingState(failedStatus)
return (
<div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2 text-sm text-destructive">
{isLoadingLogs ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<AlertTriangle className="w-3 h-3" />
)}
<span>Failed</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
{isLoadingLogs
? 'Pipeline failed - logs are being retrieved from container'
: 'Pipeline has failed - expand for error details'}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{hasDetails && (
<Collapsible open={isErrorDetailsOpen} onOpenChange={setIsErrorDetailsOpen}>
<CollapsibleTrigger asChild>
<Button type="text" size="tiny" className="h-auto py-0 px-1 text-xs">
{isErrorDetailsOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="absolute z-10 mt-1 left-0 min-w-[400px] max-w-[600px]">
<div
ref={errorDetailsRef}
className="rounded-lg border border-border bg-surface-100 p-3 shadow-md"
>
{isLoadingLogs ? (
<div className="flex items-center gap-3">
<Loader2 className="w-4 h-4 animate-spin text-warning-600" />
<div>
<div className="text-sm font-medium text-foreground mb-1">
Pipeline Failed - Loading logs...
</div>
<div className="text-xs text-foreground-light">
Error logs are being retrieved from the container. This may take a few
moments.
</div>
</div>
</div>
) : (
<>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-foreground">
Pipeline Error Details
</span>
{(failedStatus.message || failedStatus.reason) && (
<Button
type="text"
size="tiny"
onClick={async () => {
await handleCopyToClipboard(
[
failedStatus.exit_code !== undefined
? `Exit Code: ${failedStatus.exit_code}`
: null,
failedStatus.reason ? `Reason: ${failedStatus.reason}` : null,
failedStatus.message ? `Message: ${failedStatus.message}` : null,
]
.filter(Boolean)
.join('\n')
)
}}
>
<Copy className="w-3 h-3" />
</Button>
)}
</div>
<div className="space-y-2">
{failedStatus.exit_code !== undefined && (
<div>
<span className="text-xs font-medium text-foreground-light">
Exit Code:
</span>
<div className="font-mono text-sm text-foreground mt-1">
{failedStatus.exit_code}
</div>
</div>
)}
{failedStatus.reason && (
<div>
<span className="text-xs font-medium text-foreground-light">Reason:</span>
<pre className="font-mono text-sm text-foreground whitespace-pre-wrap break-words mt-1 max-h-32 overflow-y-auto">
{failedStatus.reason}
</pre>
</div>
)}
{failedStatus.message && (
<div>
<span className="text-xs font-medium text-foreground-light">
Message:
</span>
<pre className="font-mono text-sm text-foreground whitespace-pre-wrap break-words mt-1 max-h-32 overflow-y-auto">
{failedStatus.message}
</pre>
</div>
)}
</div>
</>
)}
</div>
</CollapsibleContent>
</Collapsible>
)}
</div>
)
}
// Map backend statuses to UX-friendly display
const getStatusConfig = () => {
if (requestStatus === PipelineStatusRequestStatus.EnableRequested) {
@@ -48,42 +241,59 @@ const PipelineStatus = ({
}
}
switch (pipelineStatus) {
case 'Starting':
return {
label: 'Starting',
dot: <Loader2 className="animate-spin w-3 h-3 text-warning-600" />,
color: 'text-warning-600',
tooltip: 'Pipeline is initializing and will be ready soon',
}
case 'Started':
return {
label: 'Running',
dot: <div className="w-2 h-2 bg-brand-600 rounded-full" />,
color: 'text-brand-600',
tooltip: 'Pipeline is active and processing data',
}
case 'Stopped':
return {
label: 'Stopped',
dot: <div className="w-2 h-2 bg-foreground-lighter rounded-full" />,
color: 'text-foreground-light',
tooltip: 'Pipeline is not running - enable to start processing',
}
case 'Unknown':
return {
label: 'Unknown',
dot: <div className="w-2 h-2 bg-warning-600 rounded-full" />,
color: 'text-warning-600',
tooltip: 'Pipeline status could not be determined',
}
default:
return {
label: 'Unknown',
dot: <div className="w-2 h-2 bg-destructive-600 rounded-full" />,
color: 'text-destructive-600',
tooltip: 'Pipeline status is unclear - check logs for details',
}
// Handle Failed status object
if (isFailedStatus(pipelineStatus)) {
return {
isFailedStatus: true,
}
}
if (pipelineStatus && typeof pipelineStatus === 'object' && 'name' in pipelineStatus) {
switch (pipelineStatus.name) {
case PipelineStatusName.STARTING:
return {
label: 'Starting',
dot: <Loader2 className="animate-spin w-3 h-3 text-warning-600" />,
color: 'text-warning-600',
tooltip: 'Pipeline is initializing and will be ready soon',
}
case PipelineStatusName.STARTED:
return {
label: 'Running',
dot: <div className="w-2 h-2 bg-brand-600 rounded-full" />,
color: 'text-brand-600',
tooltip: 'Pipeline is active and processing data',
}
case PipelineStatusName.STOPPED:
return {
label: 'Stopped',
dot: <div className="w-2 h-2 bg-foreground-lighter rounded-full" />,
color: 'text-foreground-light',
tooltip: 'Pipeline is not running - enable to start processing',
}
case PipelineStatusName.UNKNOWN:
return {
label: 'Unknown',
dot: <div className="w-2 h-2 bg-warning-600 rounded-full" />,
color: 'text-warning-600',
tooltip: 'Pipeline status could not be determined',
}
default:
return {
label: 'Unknown',
dot: <div className="w-2 h-2 bg-destructive-600 rounded-full" />,
color: 'text-destructive-600',
tooltip: 'Pipeline status is unclear - check logs for details',
}
}
}
// Fallback for undefined or invalid status
return {
label: 'Unknown',
dot: <div className="w-2 h-2 bg-destructive-600 rounded-full" />,
color: 'text-destructive-600',
tooltip: 'Pipeline status is unclear - check logs for details',
}
}
@@ -94,19 +304,25 @@ const PipelineStatus = ({
{isLoading && <ShimmeringLoader></ShimmeringLoader>}
{isError && <AlertError error={error} subject="Failed to retrieve pipeline status" />}
{isSuccess && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={cn('flex items-center gap-2 text-sm', statusConfig.color)}>
{statusConfig.dot}
<span>{statusConfig.label}</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{statusConfig.tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<>
{statusConfig.isFailedStatus ? (
<div className="relative">{renderFailedStatus(pipelineStatus as FailedStatus)}</div>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={cn('flex items-center gap-2 text-sm', statusConfig.color)}>
{statusConfig.dot}
<span>{statusConfig.label}</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{statusConfig.tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</>
)}
</>
)
@@ -10,9 +10,10 @@ import {
DropdownMenuTrigger,
} from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { PipelineStatusName } from './PipelineStatus'
interface RowMenuProps {
pipelineStatus: string | undefined
pipelineStatus: any
error: ResponseError | null
isLoading: boolean
isError: boolean
@@ -32,7 +33,16 @@ const RowMenu = ({
onEditClick,
onDeleteClick,
}: RowMenuProps) => {
const pipelineEnabled = pipelineStatus === 'Stopped' ? false : true
const getStatusName = (status: any) => {
if (status && typeof status === 'object' && 'name' in status) {
return status.name
}
return status
}
const statusName = getStatusName(pipelineStatus)
const pipelineEnabled = statusName !== PipelineStatusName.STOPPED
return (
<div className="flex justify-end items-center space-x-2">
{isLoading && <ShimmeringLoader></ShimmeringLoader>}
@@ -226,14 +226,6 @@ export const SchemaGraph = () => {
>
Copy as SQL
</ButtonTooltip>
<ButtonTooltip
type="default"
loading={isDownloading}
className="px-1.5"
icon={<Download />}
onClick={() => downloadImage('png')}
tooltip={{ content: { side: 'bottom', text: 'Download current view as PNG' } }}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
@@ -239,7 +239,7 @@ export const ServiceStatus = () => {
currentBranch?.status === 'MIGRATIONS_FAILED'
? 'UNHEALTHY'
: 'COMING_UP') as ProjectServiceStatus,
logsUrl: '/logs/edge-functions-logs',
logsUrl: '/branches',
},
]
: []),
@@ -39,8 +39,10 @@ export const SecretsManagement = () => {
const allSecrets = data || []
const secrets = sortBy(
searchValue.length > 0
? allSecrets.filter((secret) =>
(secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase())
? allSecrets.filter(
(secret) =>
(secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) ||
(secret?.id ?? '').toLowerCase().includes(searchValue.toLowerCase())
)
: allSecrets,
(s) => {
@@ -8,6 +8,7 @@ import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui
import SchemaSelector from 'components/ui/SchemaSelector'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import {
Button,
Form,
@@ -24,6 +25,7 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { CreateWrapperSheetProps } from './CreateWrapperSheet'
import InputField from './InputField'
import { makeValidateRequired } from './Wrappers.utils'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
const FORM_ID = 'create-wrapper-form'
@@ -63,6 +65,8 @@ export const CreateIcebergWrapperSheet = ({
onClose,
}: CreateWrapperSheetProps) => {
const { project } = useProjectContext()
const org = useSelectedOrganization()
const { mutate: sendEvent } = useSendEventMutation()
const [createSchemaSheetOpen, setCreateSchemaSheetOpen] = useState(false)
const [selectedTarget, setSelectedTarget] = useState<Target>('S3Tables')
@@ -131,6 +135,17 @@ export const CreateIcebergWrapperSheet = ({
sourceSchema: values.source_schema,
targetSchema: values.target_schema,
})
sendEvent({
action: 'foreign_data_wrapper_created',
properties: {
wrapperType: wrapperMeta.label,
},
groups: {
project: project?.ref ?? 'Unknown',
organization: org?.slug ?? 'Unknown',
},
})
}
return (
Loaded 100 of 450 files, more files were not shown because too many files have changed in this diff. Show more