Merge branch 'master' into feature/branching-merge-conflict

This commit is contained in:
Alaister Young committed 2025-07-01 17:52:01 +08:00
commit 0e85051deb
460 files changed
+30692 -4704

No files matched your search

+1 -1
View File
@@ -14,7 +14,7 @@
/docker/ @supabase/dev-workflows
/apps/studio/next.config.js @supabase/security
/apps/studio/csp.js @supabase/security
/apps/studio/components/interfaces/Billing/Payment @supabase/security
/apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
+64 -14
View File
@@ -37,6 +37,31 @@ export const preferredRegion = [
const MAX_DEPTH = 5
function isAllowedCorsOrigin(origin: string): boolean {
const exactMatches = IS_DEV
? ['http://localhost:8082', 'https://supabase.com']
: ['https://supabase.com']
if (exactMatches.includes(origin)) {
return true
}
return /^https:\/\/[\w-]+\w-supabase.vercel.app$/.test(origin)
}
function getCorsHeaders(request: Request): Record<string, string> {
const origin = request.headers.get('Origin')
if (origin && isAllowedCorsOrigin(origin)) {
return {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
}
}
return {}
}
const validationRules = [
...specifiedRules,
createQueryDepthLimiter(MAX_DEPTH),
@@ -78,13 +103,18 @@ async function handleGraphQLRequest(request: Request): Promise<NextResponse> {
const { query, variables, operationName } = parsedBody.data
const validationErrors = validateGraphQLRequest(query, isDevGraphiQL(request))
if (validationErrors.length > 0) {
return NextResponse.json({
errors: validationErrors.map((error) => ({
message: error.message,
locations: error.locations,
path: error.path,
})),
})
return NextResponse.json(
{
errors: validationErrors.map((error) => ({
message: error.message,
locations: error.locations,
path: error.path,
})),
},
{
headers: getCorsHeaders(request),
}
)
}
const result = await graphql({
@@ -94,7 +124,9 @@ async function handleGraphQLRequest(request: Request): Promise<NextResponse> {
variableValues: variables,
operationName,
})
return NextResponse.json(result)
return NextResponse.json(result, {
headers: getCorsHeaders(request),
})
}
function validateGraphQLRequest(query: string, isDevGraphiQL = false): ReadonlyArray<GraphQLError> {
@@ -112,6 +144,14 @@ function validateGraphQLRequest(query: string, isDevGraphiQL = false): ReadonlyA
return validate(rootGraphQLSchema, documentAST, rules)
}
export async function OPTIONS(request: Request): Promise<NextResponse> {
const corsHeaders = getCorsHeaders(request)
return new NextResponse(null, {
status: 204,
headers: corsHeaders,
})
}
export async function POST(request: Request): Promise<NextResponse> {
try {
const result = await handleGraphQLRequest(request)
@@ -130,18 +170,28 @@ export async function POST(request: Request): Promise<NextResponse> {
// https://github.com/getsentry/sentry-javascript/issues/9626
await Sentry.flush(2000)
return NextResponse.json({
errors: [{ message: error.isPrivate() ? 'Internal Server Error' : error.message }],
})
return NextResponse.json(
{
errors: [{ message: error.isPrivate() ? 'Internal Server Error' : error.message }],
},
{
headers: getCorsHeaders(request),
}
)
} else {
Sentry.captureException(error)
// Do not let Vercel close the process until Sentry has flushed
// https://github.com/getsentry/sentry-javascript/issues/9626
await Sentry.flush(2000)
return NextResponse.json({
errors: [{ message: 'Internal Server Error' }],
})
return NextResponse.json(
{
errors: [{ message: 'Internal Server Error' }],
},
{
headers: getCorsHeaders(request),
}
)
}
}
}
+2
View File
@@ -11,6 +11,7 @@ import { genFaviconData } from 'common/MetaFavicons/app-router'
import { GlobalProviders } from '~/features/app.providers'
import { TopNavSkeleton } from '~/layouts/MainSkeleton'
import { BASE_PATH, IS_PRODUCTION } from '~/lib/constants'
import { TelemetryTagManager } from 'common'
const metadata: Metadata = {
applicationName: 'Supabase Docs',
@@ -47,6 +48,7 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<html lang="en">
<body>
<TelemetryTagManager />
<GlobalProviders>
<TopNavSkeleton>{children}</TopNavSkeleton>
</GlobalProviders>
@@ -663,6 +663,7 @@ export const auth = {
{ name: 'Firebase Auth', url: '/guides/auth/third-party/firebase-auth' },
{ name: 'Auth0', url: '/guides/auth/third-party/auth0' },
{ name: 'AWS Cognito (Amplify)', url: '/guides/auth/third-party/aws-cognito' },
{ name: 'WorkOS', url: '/guides/auth/third-party/workos' },
],
},
{
@@ -714,6 +715,7 @@ export const auth = {
{ 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: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
{
name: 'Column Level Security',
@@ -1 +1 @@
There is no fixed fee for a Preview branch. You only pay for the usage it incurs. With Compute costs of <Price price="0.01344" /> per hour, a branch running on Micro Compute size starts at <Price price="0.32" /> per day.
There is no fixed fee for a Preview branch. You only pay for the usage it incurs. A branch running on the default Micro Compute size starts at <Price price="0.01344" /> per hour.
@@ -2,6 +2,28 @@
Go to [database.new](https://database.new) and create a new Supabase project.
Alternatively, you can create a project using the Management API:
```bash
# First, get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
# List your organizations to get the organization ID
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
https://api.supabase.com/v1/organizations
# Create a new project (replace <org-id> with your organization ID)
curl -X POST https://api.supabase.com/v1/projects \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"organization_id": "<org-id>",
"name": "My Project",
"region": "us-east-1",
"password": "<your-secure-password>"
}'
```
When your project is up and running, go to the [Table Editor](https://supabase.com/dashboard/project/_/editor), create a new table and insert some data.
Alternatively, you can run the following snippet in your project's [SQL Editor](https://supabase.com/dashboard/project/_/sql/new). This will create a `instruments` table with some sample data.
+1 -1
View File
@@ -17,7 +17,7 @@ Authentication and authorization are the core responsibilities of any Auth syste
- **Authentication** means checking that a user is who they say they are.
- **Authorization** means checking what resources a user is allowed to access.
Supabase Auth uses [JSON Web Tokens (JWTs)](/docs/guides/auth/jwts) for authentication. Auth integrates with Supabase's database features, making it easy to use [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for authorization.
Supabase Auth uses [JSON Web Tokens (JWTs)](/docs/guides/auth/jwts) for authentication. For a complete reference of all JWT fields, see the [JWT Fields Reference](/docs/guides/auth/jwt-fields). Auth integrates with Supabase's database features, making it easy to use [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security) for authorization.
## The Supabase ecosystem
@@ -29,6 +29,36 @@ The templating system provides the following variables for use:
On hosted Supabase projects, edit your email templates on the [Email Templates](/dashboard/project/_/auth/templates) page. On self-hosted projects or in local development, edit your [configuration files](/docs/guides/local-development/customizing-email-templates).
You can also manage email templates using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Get current email templates
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
| jq 'to_entries | map(select(.key | startswith("mailer_templates"))) | from_entries'
# Update email templates
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mailer_subjects_confirmation": "Confirm your signup",
"mailer_templates_confirmation_content": "<h2>Confirm your signup</h2><p>Follow this link to confirm your user:</p><p><a href=\"{{ .ConfirmationURL }}\">Confirm your email</a></p>",
"mailer_subjects_magic_link": "Your Magic Link",
"mailer_templates_magic_link_content": "<h2>Magic Link</h2><p>Follow this link to login:</p><p><a href=\"{{ .ConfirmationURL }}\">Log In</a></p>",
"mailer_subjects_recovery": "Rest Your Password",
"mailer_templates_recovery_content": "<h2>Reset Password</h2><p>Follow this link to reset the password for your user:</p><p><a href=\"{{ .ConfirmationURL }}\">Reset Password</a></p>",
"mailer_subjects_invite": "You have been invited",
"mailer_templates_invite_content": "<h2>You have been invited</h2><p>You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:</p><p><a href=\"{{ .ConfirmationURL }}\">Accept the invite</a></p>",
"mailer_subjects_email_change": "Confirm email change",
"mailer_templates_email_change_content": "<h2>Confirm email change</h2><p>Follow this link to confirm the update of your email:</p><p><a href=\"{{ .ConfirmationURL }}\">Change email</a></p>",
}'
```
## Mobile deep linking
For mobile applications, you might need to link or redirect to a specific page within your app. See the [Mobile Deep Linking guide](/docs/guides/auth/native-mobile-deep-linking) to set this up.
@@ -50,6 +50,30 @@ A non-exhaustive list of services that work with Supabase Auth is:
Once you've set up your account with an email sending service, head to the [Authentication settings page](/dashboard/project/_/settings/auth) to enable and configure custom SMTP.
You can also configure custom SMTP using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure custom SMTP
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_email_enabled": true,
"mailer_secure_email_change_enabled": true,
"mailer_autoconfirm": false,
"smtp_admin_email": "no-reply@example.com",
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_user": "your-smtp-user",
"smtp_pass": "your-smtp-password",
"smtp_sender_name": "Your App Name"
}'
```
Once you save these settings, your project's Auth server will send messages to all addresses. To protect the reputation of your newly set up service a low rate-limit of 30 messages per hour is imposed. To adjust this to an acceptable value for your use case head to the [Rate Limits configuration page](/dashboard/project/_/auth/rate-limits).
## Dealing with abuse: How to maintain the sending reputation of your SMTP server?
@@ -0,0 +1,288 @@
---
id: 'jwt-fields'
title: 'JWT Fields Reference'
subtitle: 'Complete reference for JWT fields in Supabase'
---
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.
## JWT structure overview
Supabase JWTs follow the standard JWT structure with three parts:
- **Header**: Contains algorithm and key information
- **Payload**: Contains the claims (user data and metadata)
- **Signature**: Cryptographic signature for verification
The payload contains various claims that provide user identity, authentication level, and authorization information.
## Required claims
These claims are always present in Supabase JWTs and cannot be removed:
| Field | Type | Description | Example |
| -------------- | -------------------- | ----------------------------------------------------------- | --------------------------------------------- |
| `iss` | `string` | **Issuer** - The entity that issued the JWT | `"https://project-ref.supabase.co/auth/v1"` |
| `aud` | `string \| string[]` | **Audience** - The intended recipient of the JWT | `"authenticated"` or `"anon"` |
| `exp` | `number` | **Expiration Time** - Unix timestamp when the token expires | `1640995200` |
| `iat` | `number` | **Issued At** - Unix timestamp when the token was issued | `1640991600` |
| `sub` | `string` | **Subject** - The user ID (UUID) | `"123e4567-e89b-12d3-a456-426614174000"` |
| `role` | `string` | **Role** - User's role in the system | `"authenticated"`, `"anon"`, `"service_role"` |
| `aal` | `string` | **Authenticator Assurance Level** - Authentication strength | `"aal1"`, `"aal2"` |
| `session_id` | `string` | **Session ID** - Unique session identifier | `"session-uuid"` |
| `email` | `string` | **Email** - User's email address | `"user@example.com"` |
| `phone` | `string` | **Phone** - User's phone number | `"+1234567890"` |
| `is_anonymous` | `boolean` | **Anonymous Flag** - Whether the user is anonymous | `false` |
## Optional claims
These claims may be present depending on the authentication context:
| Field | Type | Description | Example |
| --------------- | -------- | -------------------------------------------------------------------------- | --------------------------------------------------- |
| `jti` | `string` | **JWT ID** - Unique identifier for the JWT | `"jwt-uuid"` |
| `nbf` | `number` | **Not Before** - Unix timestamp before which the token is invalid | `1640991600` |
| `app_metadata` | `object` | **App Metadata** - Application-specific user data | `{"provider": "email"}` |
| `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
| Field | Type | Description | Example | Context |
| ----- | -------- | --------------------------------------------------- | ------------------------ | ----------------------------- |
| `ref` | `string` | **Project Reference** - Supabase project identifier | `"abcdefghijklmnopqrst"` | Anon/Service role tokens only |
## Field value constraints
### Authenticator assurance level (`aal`)
| Value | Description |
| -------- | ---------------------------------------------------- |
| `"aal1"` | Single-factor authentication (password, OAuth, etc.) |
| `"aal2"` | Multi-factor authentication (password + TOTP, etc.) |
### Role values (`role`)
| Value | Description | Use Case |
| ----------------- | ------------------ | ----------------------------------- |
| `"anon"` | Anonymous user | Public access with RLS policies |
| `"authenticated"` | Authenticated user | Standard user access |
| `"service_role"` | Service role | Admin privileges (server-side only) |
### Audience values (`aud`)
| Value | Description |
| ----------------- | ----------------------------- |
| `"authenticated"` | For authenticated user tokens |
| `"anon"` | For anonymous user tokens |
### Authentication methods (`amr.method`)
| Value | Description |
| ----------------- | ----------------------------- |
| `"oauth"` | OAuth provider authentication |
| `"password"` | Email/password authentication |
| `"otp"` | One-time password |
| `"totp"` | Time-based one-time password |
| `"recovery"` | Account recovery |
| `"invite"` | Invitation-based signup |
| `"sso/saml"` | SAML single sign-on |
| `"magiclink"` | Magic link authentication |
| `"email/signup"` | Email signup |
| `"email_change"` | Email change |
| `"token_refresh"` | Token refresh |
| `"anonymous"` | Anonymous authentication |
## JWT examples
### Authenticated user token
```json
{
"aal": "aal1",
"amr": [
{
"method": "password",
"timestamp": 1640991600
}
],
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"aud": "authenticated",
"email": "user@example.com",
"exp": 1640995200,
"iat": 1640991600,
"iss": "https://abcdefghijklmnopqrst.supabase.co/auth/v1",
"phone": "",
"role": "authenticated",
"session_id": "123e4567-e89b-12d3-a456-426614174000",
"sub": "123e4567-e89b-12d3-a456-426614174000",
"user_metadata": {
"name": "John Doe"
},
"is_anonymous": false
}
```
### Anonymous user token
```json
{
"iss": "supabase",
"ref": "abcdefghijklmnopqrst",
"role": "anon",
"iat": 1640991600,
"exp": 1640995200
}
```
### Service role token
```json
{
"iss": "supabase",
"ref": "abcdefghijklmnopqrst",
"role": "service_role",
"iat": 1640991600,
"exp": 1640995200
}
```
## Language-Specific considerations
### Rust
In Rust, the `ref` field is a reserved keyword. When deserializing JWTs, you'll need to handle this:
```rust
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct JwtClaims {
iss: String,
#[serde(rename = "ref")] // Handle reserved keyword
project_ref: Option<String>,
role: String,
iat: i64,
exp: i64,
// ... other fields
}
```
### TypeScript/JavaScript
```typescript
interface JwtClaims {
iss: string
aud: string | string[]
exp: number
iat: number
sub: string
role: string
aal: 'aal1' | 'aal2'
session_id: string
email: string
phone: string
is_anonymous: boolean
jti?: string
nbf?: number
app_metadata?: Record<string, any>
user_metadata?: Record<string, any>
amr?: Array<{
method: string
timestamp: number
}>
ref?: string // Only in anon/service role tokens
}
```
### Python
```python
from typing import Optional, Union, List, Dict, Any
from dataclasses import dataclass
@dataclass
class AmrEntry:
method: str
timestamp: int
@dataclass
class JwtClaims:
iss: str
aud: Union[str, List[str]]
exp: int
iat: int
sub: str
role: str
aal: str
session_id: str
email: str
phone: str
is_anonymous: bool
jti: Optional[str] = None
nbf: Optional[int] = None
app_metadata: Optional[Dict[str, Any]] = None
user_metadata: Optional[Dict[str, Any]] = None
amr: Optional[List[AmrEntry]] = None
ref: Optional[str] = None # Only in anon/service role tokens
```
### Go
```go
type AmrEntry struct {
Method string `json:"method"`
Timestamp int64 `json:"timestamp"`
}
type JwtClaims struct {
Iss string `json:"iss"`
Aud interface{} `json:"aud"` // string or []string
Exp int64 `json:"exp"`
Iat int64 `json:"iat"`
Sub string `json:"sub"`
Role string `json:"role"`
Aal string `json:"aal"`
SessionID string `json:"session_id"`
Email string `json:"email"`
Phone string `json:"phone"`
IsAnonymous bool `json:"is_anonymous"`
Jti *string `json:"jti,omitempty"`
Nbf *int64 `json:"nbf,omitempty"`
AppMetadata map[string]interface{} `json:"app_metadata,omitempty"`
UserMetadata map[string]interface{} `json:"user_metadata,omitempty"`
Amr []AmrEntry `json:"amr,omitempty"`
Ref *string `json:"ref,omitempty"` // Only in anon/service role tokens
}
```
## Validation guidelines
When implementing JWT validation on your server:
1. **Check Required Fields**: Ensure all required claims are present
2. **Validate Types**: Verify field types match expected types
3. **Check Expiration**: Validate `exp` timestamp is in the future
4. **Verify Issuer**: Ensure `iss` matches your Supabase project
5. **Check Audience**: Validate `aud` matches expected audience
6. **Handle Reserved Keywords**: Use field renaming for languages like Rust
## Security considerations
- **Always validate the JWT signature** before trusting any claims
- **Never expose service role tokens** to client-side code
- **Validate all claims** before trusting the JWT
- **Check token expiration** on every request
- **Use HTTPS** for all JWT transmission
- **Rotate JWT secrets** regularly
- **Implement proper error handling** for invalid tokens
## Related documentation
- [JWT Overview](/docs/guides/auth/jwts)
- [Custom Access Token Hooks](/docs/guides/auth/auth-hooks/custom-access-token-hook)
- [Row Level Security](/docs/guides/database/postgres/row-level-security)
- [Server-Side Auth](/docs/guides/auth/server-side)
+1
View File
@@ -177,3 +177,4 @@ Now that you understand what JWTs are and where they're used in Supabase, you ca
## Resources
- JWT debugger: https://jwt.io/
- [JWT Fields Reference](/docs/guides/auth/jwt-fields) - Complete reference for all JWT fields in Supabase
@@ -5,4 +5,31 @@ subtitle: 'Rate limits protect your services from abuse'
Supabase Auth enforces rate limits on endpoints to prevent abuse. Some rate limits are [customizable](/dashboard/project/_/auth/rate-limits).
You can also manage rate limits using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Get current rate limits
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
| jq 'to_entries | map(select(.key | startswith("rate_limit_"))) | from_entries'
# Update rate limits
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rate_limit_anonymous_users": 10,
"rate_limit_email_sent": 10,
"rate_limit_sms_sent": 10,
"rate_limit_verify": 10,
"rate_limit_token_refresh": 10,
"rate_limit_otp": 10,
"rate_limit_web3": 10
}'
```
<$Partial path="auth_rate_limits.mdx" />
@@ -321,7 +321,7 @@ Set up a listener for Auth events on the client, to handle session refreshes and
<StepHikeCompact.Details title="Create your first page">
Create your first page. This example page calls Supabase from the server to get a list of countries from the database.
Create your first page. This example page calls Supabase from the server to get a list of colors from the database.
This is an example of a public page that uses publicly readable data.
@@ -67,6 +67,24 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app,
6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever lose access to it, or make it public accidentally, revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring meeting in your calendar!
7. Finally, add the information you configured above to the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers).
You can also configure the Apple auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Apple auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_apple_enabled": true,
"external_apple_client_id": "your-services-id",
"external_apple_secret": "your-generated-secret-key"
}'
```
<Admonition type="tip">
Use this tool to generate a new Apple client secret. No keys leave your browser! Be aware that this tool does not currently work in Safari, so use Firefox or a Chrome-based browser instead.
@@ -44,6 +44,25 @@ Setting up OAuth with Azure consists of four broad steps:
![Obtain the client secret](/docs/img/guides/auth-azure/azure-client-secret.png)
You can also configure the Azure auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Azure auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_azure_enabled": true,
"external_azure_client_id": "your-azure-client-id",
"external_azure_secret": "your-azure-client-secret",
"external_azure_url": "your-azure-url"
}'
```
## Guarding against unverified email domains
Microsoft Entra ID can send out unverified email domains in certain cases. This may open up your project to a vulnerability where a malicious user can impersonate already existing accounts on your project.
@@ -43,6 +43,24 @@ Setting up Discord logins for your application consists of 3 parts:
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Discord" }} />
You can also configure the Discord auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Discord auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_discord_enabled": true,
"external_discord_client_id": "your-discord-client-id",
"external_discord_secret": "your-discord-client-secret"
}'
```
## Add login code to your client app
<Tabs
@@ -60,6 +60,24 @@ Under `Build Your App`, click on `Use Cases` screen. From there, do the followin
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Facebook" }} />
You can also configure the Facebook auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Facebook auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_facebook_enabled": true,
"external_facebook_client_id": "your-facebook-app-id",
"external_facebook_secret": "your-facebook-app-secret"
}'
```
## Add login code to your client app
<Tabs
@@ -38,6 +38,24 @@ Copy your new OAuth credentials
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "GitHub" }} />
You can also configure the GitHub auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure GitHub auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_github_enabled": true,
"external_github_client_id": "your-github-client-id",
"external_github_secret": "your-github-client-secret"
}'
```
## Add login code to your client app
<Tabs
@@ -53,6 +53,24 @@ To use your own application code:
1. When you finish configuring your credentials, you will be shown your client ID and secret. Add these to the [Google Auth Provider section of the Supabase Dashboard](/dashboard/project/_/auth/providers).
Alternatively, you can configure Google authentication using the Management API:
```bash
# First, get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Update auth config to enable Google provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_google_enabled": true,
"external_google_client_id": "your-google-client-id",
"external_google_secret": "your-google-client-secret"
}'
```
<Admonition type="tip">
In local development, you can add the client ID and secret to your `config.toml` file.
@@ -46,6 +46,24 @@ Ensure that the appropriate scopes have been added under OAuth 2.0 Scopes at the
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "LinkedIn (OIDC)" }} />
You can also configure the LinkedIn (OIDC) auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure LinkedIn (OIDC) auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_linkedin_oidc_enabled": true,
"external_linkedin_oidc_client_id": "your-linkedin-client-id",
"external_linkedin_oidc_secret": "your-linkedin-client-secret"
}'
```
## Add login code to your client app
<Tabs
@@ -62,6 +62,24 @@ Under `Scopes`:
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Slack" }} />
You can also configure the Slack (OIDC) auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Slack (OIDC) auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_slack_oidc_enabled": true,
"external_slack_oidc_client_id": "your-slack-client-id",
"external_slack_oidc_secret": "your-slack-client-secret"
}'
```
## Add login code to your client app
<Tabs
@@ -48,6 +48,24 @@ Under `Redirect URIs`:
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Spotify" }} />
You can also configure the Spotify auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Spotify auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_spotify_enabled": true,
"external_spotify_client_id": "your-spotify-client-id",
"external_spotify_secret": "your-spotify-client-secret"
}'
```
## Add login code to your client app
The following outlines the steps to sign in using Spotify with Supabase Auth.
@@ -50,6 +50,24 @@ Setting up Twitter logins for your application consists of 3 parts:
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Twitter" }} />
You can also configure the Twitter auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Twitter auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_twitter_enabled": true,
"external_twitter_client_id": "your-twitter-api-key",
"external_twitter_secret": "your-twitter-api-secret-key"
}'
```
## Add login code to your client app
<Tabs
@@ -40,6 +40,25 @@ You must be signed in to see these values.
1. Copy the `Callback URL (for OAuth)` value from the form and save it somewhere handy.
1. Click Save.
You can also configure the WorkOS auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure WorkOS auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_workos_enabled": true,
"external_workos_url": "https://api.workos.com",
"external_workos_client_id": "your-workos-client-id",
"external_workos_secret": "your-workos-client-secret"
}'
```
## Step 4. Set your Supabase redirect URI in the WorkOS Dashboard
Visit the WorkOS dashboard and click the redirects button in the left navigation panel.
@@ -57,6 +57,24 @@ Under `Scopes`
<$Partial path="social_provider_settings_supabase.mdx" variables={{ "provider": "Zoom" }} />
You can also configure the Zoom auth provider using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Configure Zoom auth provider
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_zoom_enabled": true,
"external_zoom_client_id": "your-zoom-client-id",
"external_zoom_secret": "your-zoom-client-secret"
}'
```
## Add login code to your client app
<Tabs
@@ -10,6 +10,7 @@ Supabase has first-class support for these third-party authentication providers:
- [Firebase Auth](/docs/guides/auth/third-party/firebase-auth)
- [Auth0](/docs/guides/auth/third-party/auth0)
- [AWS Cognito (with or without AWS Amplify)](/docs/guides/auth/third-party/aws-cognito)
- [WorkOS](/docs/guides/auth/third-party/workos)
You can use these providers alongside Supabase Auth, or on their own, to access the [Data API (REST and GraphQL)](/docs/guides/database), [Storage](/docs/guides/storage), [Realtime](/docs/guides/storage) and [Functions](/docs/guides/functions) from your existing apps.
+57
View File
@@ -0,0 +1,57 @@
---
id: 'auth-third-party-workos'
title: 'WorkOS'
subtitle: 'Use WorkOS with your Supabase project'
---
WorkOS can be used as a third-party authentication provider alongside Supabase Auth, or standalone, with your Supabase project.
## Getting started
1. First you need to add an integration to connect your Supabase project with your WorkOS tenant. You will need your WorkOS issuer. The issuer is `https://api.workos.com/user_management/<your-client-id>`. Substitute your [custom auth domain](https://workos.com/docs/custom-domains/auth-api) for "api.workos.com" if configured.
2. Add a new Third-party Auth integration in your project's [Authentication settings](/dashboard/project/_/settings/auth).
3. Set up a JWT template to assign the `role: 'authenticated'` claim to your access token.
## Setup the Supabase client library
<Tabs type="underlined" queryGroup="language">
<TabPanel id="ts" label="TypeScript">
```typescript
import { createClient } from '@supabase/supabase-js'
import { createClient as createAuthKitClient } from '@workos-inc/authkit-js'
const authkit = await createAuthKitClient('WORKOS_CLIENT_ID', {
apiHostname: '<WORKOS_AUTH_DOMAIN>',
})
const supabase = createClient('https://<supabase-project>.supabase.co', 'SUPABASE_ANON_KEY', {
accessToken: async () => {
return authkit.getAccessToken()
},
})
```
</TabPanel>
</Tabs>
## Add a new Third-Party Auth integration to your project
In the dashboard navigate to your project's [Authentication settings](/dashboard/project/_/settings/auth) and find the Third-Party Auth section to add a new integration.
## Set up a JWT template to add the authenticated role.
Your Supabase project inspects the `role` claim present in all JWTs sent to it, to assign the correct Postgres role when using the Data API, Storage or Realtime authorization.
WorkOS JWTs already contain a `role` claim that corresponds to the user's role in their organization. It is necessary to adjust the `role` claim to be `"authenticated"` like Supabase expects. This can be done using JWT templates (navigate to Authentication -> Sessions -> JWT Template in the WorkOS Dashboard).
This template overrides the `role` claim to meet Supabase's expectations, and adds the WorkOS role in a new `user_role` claim:
```json
{
"role": "authenticated",
"user_role": {{organization_membership.role}}
}
```
@@ -6,7 +6,7 @@ description: 'Scalable time-series data storage and analysis'
<Admonition type="deprecation">
The `timescaledb` extension is deprecated in projects using Postgres 17. It continues to be supported in projects using Postgres 15, but will need to dropped before those projects are upgraded to Postgres 17. See the [Upgrading to Postgres 17 notes](/guides/platform/upgrading#upgrading-to-postgres-17) for more information.
The `timescaledb` extension is deprecated in projects using Postgres 17. It continues to be supported in projects using Postgres 15, but will need to dropped before those projects are upgraded to Postgres 17. See the [Upgrading to Postgres 17 notes](/docs/guides/platform/upgrading#upgrading-to-postgres-17) for more information.
</Admonition>
@@ -29,7 +29,7 @@ select pg_create_logical_replication_slot('example_slot', 'pgoutput');
This will need a **direct** connection (not a Connection Pooler) to your database and you can find the connection info in the [Dashboard](https://supabase.com/dashboard/project/_/settings/database).
You will also need to ensure that IPv6 is supported by your replication destination (or you can enable the [IPv4 add-on](/guides/platform/ipv4-address))
You will also need to ensure that IPv6 is supported by your replication destination (or you can enable the [IPv4 add-on](/docs/guides/platform/ipv4-address))
If you would prefer not to use the `postgres` user, then you can run `CREATE ROLE <user> WITH REPLICATION;` using the `postgres` user.
@@ -59,7 +59,7 @@ Logical replication is typically output in 2 forms, `pgoutput` and `wal2json`. T
When using logical replication, Postgres is then configured to keep WAL files around for longer than it needs them. If the files are removed too quickly, then your `replication slot` will become inactive and, if the database receives a large number of changes in a short time, then the `replication slot` can become lost as it was not able to keep up.
In order to mitigate this, Postgres has many options and settings that can be [tweaked](/guides/database/custom-postgres-config) to manage the WAL usage effectively. Not all of these settings are user configurable as they can impact the stability of your database. For those that are, these should be considered as advanced configuration and not changed without understanding that they can cause additional disk space and resources to be used, as well as incur additional costs.
In order to mitigate this, Postgres has many options and settings that can be [tweaked](/docs/guides/database/custom-postgres-config) to manage the WAL usage effectively. Not all of these settings are user configurable as they can impact the stability of your database. For those that are, these should be considered as advanced configuration and not changed without understanding that they can cause additional disk space and resources to be used, as well as incur additional costs.
| Setting | Description | User-facing | Default |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------- | ------- |
@@ -13,7 +13,7 @@ Connections through a pooler, such as Supavisor, will not work.
# The tool in use does not support IPv6
You can enable the [IPv4 add-on](/guides/platform/ipv4-address) for your project.
You can enable the [IPv4 add-on](/docs/guides/platform/ipv4-address) for your project.
# What is XMIN and should it be used?
@@ -29,7 +29,7 @@ You can view [publications](https://supabase.com/dashboard/project/default/datab
# How to configure database settings for replication?
Yes. Using the Supabase CLI, you can [configure database settings](/guides/database/custom-postgres-config#cli-configurable-settings) to optimize them for your replication needs. These values can vary depending on the activity of your database size and activity.
Yes. Using the Supabase CLI, you can [configure database settings](/docs/guides/database/custom-postgres-config#cli-configurable-settings) to optimize them for your replication needs. These values can vary depending on the activity of your database size and activity.
# What are some important configuration options?
@@ -4,12 +4,12 @@ title: 'Monitoring replication'
Monitoring replication lag is important and there are 3 ways to do this:
1. Dashboard - Under the [Reports](/guides/platform/reports) of the dashboard, you can view the replication lag of your project
1. Dashboard - Under the [Reports](/docs/guides/platform/reports) of the dashboard, you can view the replication lag of your project
2. Database -
- pg_stat_subscription (subscriber) - if PID is null, then the subscription is not active
- pg_stat_subscription_stats - look here for error_count to see if there were issues applying or syncing (if yes, check the logs for why)
- pg_replication_slots - use this to check if the slot is active and you can also calculate the lag from here
3. [Metrics](/guides/telemetry/metrics) - Using the prometheus endpoint for your project
3. [Metrics](/docs/guides/telemetry/metrics) - Using the prometheus endpoint for your project
- replication_slots_max_lag_bytes - this is the more important one
- pg_stat_replication_replay_lag - lag to replay WAL files from the source DB on the target DB (throttled by disk or high activity)
- pg_stat_replication_send_lag - lag in sending WAL files from the source DB (a high lag means that the publisher is not being asked to send new WAL files OR a network issues)
@@ -68,7 +68,7 @@ Materialize has the following [documentation](https://materialize.com/docs/sql/c
You can follow those steps with the following modifications:
1. Follow the steps in our [guide](/guides/database/postgres/setup-replication-external) to create a publication slot
1. Follow the steps in our [guide](/docs/guides/database/postgres/setup-replication-external) to create a publication slot
</TabPanel>
@@ -716,7 +716,7 @@ You can make a similar setup with a distinct project for each environment. Or ju
Branching is available on the Pro Plan and above. The price is:
- Each Preview branch costs <Price price="0.32" /> per day
- Each Preview branch costs <Price price="0.01344" /> per hour
- Each Preview branch is billed until it is removed
## Troubleshooting
@@ -75,6 +75,35 @@ You will also need to set secrets for your production Edge Functions. You can do
}}
/>
### Using the Management API
You can also manage secrets programmatically using the Management API:
```bash
# First, get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Create a secret
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/secrets" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '[{
"name": "MY_SECRET_NAME",
"value": "my-secret-value"
}]'
# List all secrets
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$PROJECT_REF/secrets"
# Delete a secret
curl -X DELETE "https://api.supabase.com/v1/projects/$PROJECT_REF/secrets" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '["MY_SECRET_NAME"]'
```
### Using the CLI
Let's create a `.env` to help us deploy our secrets to production. In this case we'll just use the same as our local secrets:
@@ -29,7 +29,7 @@ Each Supabase project consists of several tools:
### Postgres (database)
Postgres is the core of Supabase. We do not abstract the Postgres database—you can access it and use it with full privileges. We provide tools which makes Postgres as easy to use as Firebase.
Postgres is the core of Supabase. We do not abstract the Postgres database—you can access it and use it with full privileges. We provide tools which make Postgres as easy to use as Firebase.
- Official Docs: [postgresql.org/docs](https://www.postgresql.org/docs/current/index.html)
- Source code: [github.com/postgres/postgres](https://github.com/postgres/postgres) (mirror)
@@ -64,7 +64,7 @@ Deploy read-only databases across multiple regions, for lower latency and better
### Log drains
Export Supabase logs at to 3rd party providers and external tooling. [Docs](/docs/guides/platform/log-drains).
Export Supabase logs to 3rd party providers and external tooling. [Docs](/docs/guides/platform/log-drains).
## Studio
@@ -195,53 +195,56 @@ Features in Beta are tested by an external penetration tester for security issue
In addition to the Beta requirements, features in GA are covered by the [uptime SLA](https://supabase.com/sla).
| Product | Feature | Stage | Available on self-hosted |
| -------------- | -------------------------- | --------------- | ------------------------------------------- |
| Database | Postgres | `GA` | ✅ |
| Database | Vector Database | `GA` | ✅ |
| Database | Auto-generated Rest API | `GA` | ✅ |
| Database | Auto-generated GraphQL API | `GA` | ✅ |
| Database | Webhooks | `beta` | ✅ |
| Database | Vault | `public alpha` | ✅ |
| Platform | | `GA` | ✅ |
| Platform | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
| Platform | Custom Domains | `GA` | N/A |
| Platform | Network Restrictions | `beta` | N/A |
| Platform | SSL enforcement | `GA` | N/A |
| Platform | Branching | `beta` | N/A |
| Platform | Terraform Provider | `public alpha` | N/A |
| Platform | Read Replicas | `private alpha` | N/A |
| Platform | Log Drains | `public alpha` | ✅ |
| Studio | | `GA` | ✅ |
| Studio | SSO | `GA` | ✅ |
| Realtime | Postgres Changes | `GA` | ✅ |
| Realtime | Broadcast | `GA` | ✅ |
| Realtime | Presence | `GA` | ✅ |
| Realtime | Broadcast Authorization | `public beta` | ✅ |
| Realtime | Presence Authorization | `public beta` | ✅ |
| Storage | | `GA` | ✅ |
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Smart CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Image Transformations | `GA` | ✅ |
| Storage | Resumable Uploads | `GA` | ✅ |
| Storage | S3 compatibility | `public alpha` | ✅ |
| Edge Functions | | `beta` | ✅ |
| Edge Functions | Regional Invocations | `beta` | ✅ |
| Edge Functions | NPM compatibility | `beta` | ✅ |
| Auth | | `GA` | ✅ |
| Auth | Email login | `GA` | ✅ |
| Auth | Social login | `GA` | ✅ |
| Auth | Phone login | `GA` | ✅ |
| Auth | Passwordless login | `GA` | ✅ |
| Auth | SSO with SAML | `GA` | ✅ |
| Auth | Authorization via RLS | `GA` | ✅ |
| Auth | CAPTCHA protection | `GA` | ✅ |
| Auth | Server-side Auth | `beta` | ✅ |
| CLI | | `GA` | ✅ Works with self-hosted |
| Management API | | `GA` | N/A |
| Client Library | JavaScript | `GA` | N/A |
| Client Library | Flutter | `beta` | N/A |
| Client Library | Swift | `beta` | N/A |
| Product | Feature | Stage | Available on self-hosted |
| -------------- | -------------------------- | -------------- | ------------------------------------------- |
| Database | Postgres | `GA` | ✅ |
| Database | Vector Database | `GA` | ✅ |
| Database | Auto-generated Rest API | `GA` | ✅ |
| Database | Auto-generated GraphQL API | `GA` | ✅ |
| Database | Webhooks | `beta` | ✅ |
| Database | Vault | `public alpha` | ✅ |
| Platform | | `GA` | ✅ |
| Platform | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
| Platform | Custom Domains | `GA` | N/A |
| Platform | Network Restrictions | `GA` | N/A |
| Platform | SSL enforcement | `GA` | N/A |
| Platform | Branching | `beta` | N/A |
| Platform | Terraform Provider | `public alpha` | N/A |
| Platform | Read Replicas | `GA` | N/A |
| Platform | Log Drains | `public alpha` | ✅ |
| Studio | | `GA` | ✅ |
| Studio | SSO | `GA` | ✅ |
| Realtime | Postgres Changes | `GA` | ✅ |
| Realtime | Broadcast | `GA` | ✅ |
| Realtime | Presence | `GA` | ✅ |
| Realtime | Broadcast Authorization | `public beta` | ✅ |
| Realtime | Presence Authorization | `public beta` | ✅ |
| Realtime | Broadcast from Database | `public beta` | ✅ |
| Storage | | `GA` | ✅ |
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Smart CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
| Storage | Image Transformations | `GA` | ✅ |
| Storage | Resumable Uploads | `GA` | ✅ |
| Storage | S3 compatibility | `GA` | ✅ |
| Edge Functions | | `GA` | ✅ |
| Edge Functions | Regional Invocations | `GA` | ✅ |
| Edge Functions | NPM compatibility | `GA` | ✅ |
| Auth | | `GA` | ✅ |
| Auth | Email login | `GA` | ✅ |
| Auth | Social login | `GA` | ✅ |
| Auth | Phone login | `GA` | ✅ |
| Auth | Passwordless login | `GA` | ✅ |
| Auth | SSO with SAML | `GA` | ✅ |
| Auth | Authorization via RLS | `GA` | ✅ |
| Auth | CAPTCHA protection | `GA` | ✅ |
| Auth | Server-side Auth | `beta` | ✅ |
| Auth | Third-Party Auth | `GA` | ✅ |
| CLI | | `GA` | ✅ Works with self-hosted |
| Management API | | `GA` | N/A |
| Client Library | JavaScript | `GA` | N/A |
| Client Library | Flutter | `GA` | N/A |
| Client Library | Swift | `GA` | N/A |
| Client Library | Python | `beta` | N/A |
- ✅ = Fully Available
- 🚧 = Available, but requires external tools or configuration
@@ -14,6 +14,7 @@ The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP)
- [Cline](#cline) (VS Code extension)
- [Claude desktop](#claude-desktop)
- [Claude code](#claude-code)
- [Amp](#amp)
Once connected, your AI assistants can interact with and query your Supabase projects on your behalf.
@@ -117,11 +118,84 @@ You can also add the Supabase MCP server as a locally-scoped server, which will
1. Run the following command in your terminal:
```bash
claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN=your_token_here npx -y @supabase/mcp-server-supabase@latest
claude mcp add supabase -s local -e SUPABASE_ACCESS_TOKEN=your_token_here -- npx -y @supabase/mcp-server-supabase@latest
```
Locally-scoped servers take precedence over project-scoped servers with the same name and are stored in your project-specific user settings.
### Amp
You can add the Supabase MCP server to Amp in two ways:
#### Option 1: VSCode settings.json
1. Open "Preferences: Open User Settings (JSON)"
2. Add the following configuration:
```json
{
"amp.mcpServers": {
"supabase": {
"command": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--read-only",
"--project-ref=<project-ref>"
],
"env": {
"SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
}
}
}
}
```
Replace `project-ref` and `personal-access-token` with your project ref and personal access token.
3. Save the configuration file.
4. Restart VS Code to apply the new configuration.
#### Option 2: Amp CLI
1. Edit `~/.config/amp/settings.json`
1. Add the following configuration:
```json
{
"amp.mcpServers": {
"supabase": {
"command": "npx",
"args": [
"-y",
"@supabase/mcp-server-supabase@latest",
"--read-only",
"--project-ref=<project-ref>"
],
"env": {
"SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
}
}
}
}
```
Replace `project-ref` and `personal-access-token` with your project ref and personal access token.
1. Save the configuration file.
1. Restart Amp to apply the new configuration.
### Qodo Gen
1. Open [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ.
1. Click `Connect more tools`.
1. Click `+ Add new MCP`.
1. Add the following configuration:
<$Partial path="mcp_supabase_config.mdx" variables={{ "app": "Qodo Gen" }} />
1. Click `Save`.
### Next steps
Your AI tool is now connected to Supabase using MCP. Try asking your AI assistant to create a new project, create a table, or fetch project config.
@@ -32,13 +32,49 @@ content_path = "./supabase/templates/invite.html"
## Available email templates
There are several Auth email templates which can be configured:
There are several Auth email templates which can be configured. Each template serves a specific authentication flow:
- `auth.email.template.invite`
- `auth.email.template.confirmation`
- `auth.email.template.recovery`
- `auth.email.template.magic_link`
- `auth.email.template.email_change`
### `auth.email.template.invite`
**Default subject**: "You have been invited"
**When sent**: When a user is invited to join your application via email invitation
**Purpose**: Allows administrators to invite users who don't have accounts yet
**Content**: Contains a link for the invited user to accept the invitation and create their account
### `auth.email.template.confirmation`
**Default subject**: "Confirm Your Signup"
**When sent**: When a user signs up and needs to verify their email address
**Purpose**: Email verification for new user registrations
**Content**: Contains a confirmation link to verify the user's email address
### `auth.email.template.recovery`
**Default subject**: "Reset Your Password"
**When sent**: When a user requests a password reset
**Purpose**: Password recovery flow for users who forgot their password
**Content**: Contains a link to reset the user's password
### `auth.email.template.magic_link`
**Default subject**: "Your Magic Link"
**When sent**: When a user requests a magic link for passwordless authentication
**Purpose**: Passwordless login using email links
**Content**: Contains a secure link that automatically logs the user in when clicked
### `auth.email.template.email_change`
**Default subject**: "Confirm Email Change"
**When sent**: When a user requests to change their email address
**Purpose**: Verification for email address changes
**Content**: Contains a confirmation link to verify the new email address
### `auth.email.template.reauthentication`
**Default subject**: "Confirm Reauthentication"
**When sent**: When a user needs to re-authenticate for sensitive operations
**Purpose**: Additional verification for sensitive actions (like changing password, deleting account)
**Content**: Contains a 6-digit OTP code for verification
## Template variables
@@ -98,28 +98,6 @@ supabase migration up
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Stop the local database">
Before updating your schema files, stop the local development environment.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
<$CodeTabs>
```bash name=Terminal
supabase stop
```
</$CodeTabs>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
</StepHikeCompact>
<StepHikeCompact>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Add a new column">
Edit `supabase/schemas/employees.sql` file to add a new column to `employees` table.
</StepHikeCompact.Details>
@@ -151,7 +129,7 @@ Some entities like views and enums expect columns to be declared in a specific o
<StepHikeCompact>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Generate a new migration">
Diff existing migrations against your declared schema.
</StepHikeCompact.Details>
@@ -173,7 +151,7 @@ supabase db diff -f add_age
<StepHikeCompact>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Review the generated migration">
Verify that the generated migration contain a single incremental change.
</StepHikeCompact.Details>
@@ -194,7 +172,7 @@ alter table "public"."employees" add column "age" smallint not null;
</StepHikeCompact>
<StepHikeCompact>
<StepHikeCompact.Step step={5}>
<StepHikeCompact.Step step={4}>
<StepHikeCompact.Details title="Apply the pending migration">
Start the database locally and apply the pending migration.
</StepHikeCompact.Details>
@@ -204,7 +182,7 @@ alter table "public"."employees" add column "age" smallint not null;
<$CodeTabs>
```bash name=Terminal
supabase start && supabase migration up
supabase migration up
```
</$CodeTabs>
@@ -56,3 +56,13 @@ client_id = "env(GITHUB_CLIENT_ID)"
secret = "env(GITHUB_SECRET)"
redirect_uri = "" # Overrides the default auth redirectUrl.
```
### Going further
For more advanced secrets management workflows, including:
- **Using dotenvx for encrypted secrets**: Learn how to securely manage environment variables across different branches and environments
- **Branch-specific secrets**: Understand how to manage secrets for different deployment environments
- **Encrypted configuration values**: Use encrypted values directly in your `config.toml`
See the [Managing secrets for branches](/docs/guides/deployment/branching#managing-secrets-for-branches) section in our branching documentation, or check out the [dotenvx example repository](https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone-dotenvx/README.md) for a complete implementation.
@@ -32,6 +32,20 @@ This is a security measure to prevent accidental invites to accounts not managed
</Admonition>
### Viewing organization members using the Management API
You can also view organization members using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export ORG_ID="your-organization-id"
# List organization members
curl "https://api.supabase.com/v1/organizations/$ORG_ID/members" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
```
### Transferring ownership of an organization
Each Supabase organization must have at least one owner. If your organization has other owners then you can relinquish ownership and leave the organization by clicking **Leave team** in your organization's team [settings](/dashboard/org/_/team).
@@ -71,6 +71,26 @@ The Postgres utility [pg_dumpall](https://www.postgresql.org/docs/current/app-pg
You can access daily backups in the [Scheduled backups](https://supabase.com/dashboard/project/_/database/backups/scheduled) settings in the Dashboard. Pro Plan projects can access the last 7 days' worth of daily backups. Team Plan projects can access the last 14 days' worth of daily backups, while Enterprise Plan projects can access up to 30 days' worth of daily backups. Users can restore their project to any one of the backups. If you wish to generate a logical backup on your own, you can do so through the [Supabase CLI](/docs/reference/cli/supabase-db-dump).
You can also manage backups programmatically using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# List all available backups
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups"
# Restore from a PITR (not logical) backup (replace ISO timestamp with desired restore point)
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/database/backups/restore-pitr" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"recovery_time_target_unix": "1735689600"
}'
```
#### Backup process for large databases
Databases larger than 15GB[^1], if they're on a recent build[^2] of the Supabase platform, get automatically transitioned[^3] to use daily physical backups. Physical backups are a more performant backup mechanism that lowers the overhead and impact on the database being backed up, and also avoids holding locks on objects in your database for a long period of time. While restores are unaffected, the backups created using this method cannot be downloaded from the Backups section of the dashboard.
@@ -28,6 +28,30 @@ IPv4 addresses are guaranteed to be static for ingress traffic. If your database
You can enable the IPv4 add-on in your project's [add-ons settings](/dashboard/project/_/settings/addons).
You can also manage the IPv4 add-on using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Get current IPv4 add-on status
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/billing/addons" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
# Enable IPv4 add-on
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/addons" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"addon_type": "ipv4"
}'
# Disable IPv4 add-on
curl -X DELETE "https://api.supabase.com/v1/projects/$PROJECT_REF/billing/addons/ipv4" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
```
<Admonition type="caution">
Note that direct database connections can experience a short amount of downtime when toggling the
add-on due to DNS reconfiguration and propagation. Generally, this should be less than a minute.
@@ -18,6 +18,30 @@ If direct connections to your database [resolve to a IPv6 address](https://supab
Network restrictions can be configured in the [Database Settings](https://supabase.com/dashboard/project/_/settings/database) page. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling network restrictions.
## To get started via the Management API:
You can also manage network restrictions using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Get current network restrictions
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/network-restrictions" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
# Update network restrictions
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/network-restrictions/apply" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"db_allowed_cidrs": [
"192.168.0.1/24",
]
}'
```
## To get started via the CLI:
1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+.
@@ -51,6 +51,30 @@ Projects must meet these requirements to use Read Replicas:
To add a Read Replica, go to the [Infrastructure Settings page](/dashboard/project/_/settings/infrastructure) in your dashboard.
You can also manage Read Replicas using the Management API (beta functionality):
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Create a new Read Replica
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/read-replicas/setup" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"region": "us-east-1"
}'
# Delete a Read Replica
curl -X POST "https://api.supabase.com/v1/projects/$PROJECT_REF/read-replicas/remove" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"database_identifier": "abcdefghijklmnopqrst"
}'
```
Projects on an XL compute add-on or larger can create up to five Read Replicas. Projects on compute add-ons smaller than XL can create up to two Read Replicas. All Read Replicas inherit the compute size of their Primary database.
### Deploying a Read Replica
@@ -18,6 +18,40 @@ Projects need to be at least on Postgres 13.3.0 to enable SSL enforcement. You c
SSL enforcement can be configured via the "Enforce SSL on incoming connections" setting under the SSL Configuration section in [Database Settings page](https://supabase.com/dashboard/project/_/settings/database) of the dashboard.
## Manage SSL enforcement via the Management API
You can also manage SSL enforcement using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Get current SSL enforcement status
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/ssl-enforceemnt" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
# Enable SSL enforcement
curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/ssl-enforcement" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestedConfig": {
"database": true
}
}'
# Disable SSL enforcement
curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/ssl-enforcement" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestedConfig": {
"database": false
}
}'
```
## Manage SSL enforcement via the CLI
To get started:
+22 -2
View File
@@ -22,10 +22,30 @@ The endpoint discussed in this article is not available on self-hosted.
## Accessing the metrics endpoint
Your project's metrics endpoint is accessible at `https://<project-ref>.supabase.co/customer/v1/privileged/metrics`. Access to the endpoint is secured via HTTP Basic Auth; the username is `service_role`, while the password is the service role JWT available through the Supabase dashboard.
Your project's metrics endpoint is accessible at `https://<project-ref>.supabase.co/customer/v1/privileged/metrics`.
Access to the endpoint is secured via HTTP Basic Auth:
- username: `service_role`
- password: the service role JWT from the [Supabase dashboard](https://supabase.com/dashboard/project/_/settings/api-keys)
You can also retrieve your service role key programmatically using the Management API:
```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"
# Get project API keys including service_role key
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$PROJECT_REF/api-keys?reveal=true"
```
<ProjectConfigVariables variable="url" />
```shell
> curl https://<project-ref>.supabase.co/customer/v1/privileged/metrics --user 'service_role:<service-role-jwt>'
curl <project-url>/customer/v1/privileged/metrics \
--user 'service_role:<service-role-jwt>'
```
## Supabase Grafana
+4 -4
View File
@@ -895,14 +895,14 @@ parameters:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
- id: 'auth.email.otp_exp'
title: 'auth.email.otp_exp'
- id: 'auth.email.otp_expiry'
title: 'auth.email.otp_expiry'
tags: ['auth']
required: false
default: '300'
default: '3600'
description: |
The expiry time for an OTP code in seconds.
Default is 300 seconds (5 minutes).
Default is 3600 seconds (1 hour).
links:
- name: 'Auth Server configuration'
link: 'https://supabase.com/docs/reference/auth'
+1 -1
View File
@@ -4406,7 +4406,7 @@ functions:
name: Call a read-only Postgres function
code: |
```ts
const { data, error } = await supabase.rpc('hello_world', { get: true })
const { data, error } = await supabase.rpc('hello_world', undefined, { get: true })
```
data:
sql: |
+1
View File
@@ -33,6 +33,7 @@
"NEXT_PUBLIC_AUTH_PERSISTED_KEY",
"NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY",
"NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL",
"NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID",
"NEXT_PUBLIC_GOTRUE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
// These envs are technically passthrough env vars because they're only used on the server side of Nextjs
+2
View File
@@ -37,3 +37,5 @@ yarn-error.log*
/public/dashboard
# Sentry Auth Token
.sentryclirc
certificates
@@ -79,7 +79,7 @@ export function ApiKeyPill({
async function onCopy() {
// If key is already revealed, use that value
if (data?.api_key) return data?.api_key
if (data?.api_key) return data?.api_key ?? ''
try {
// Fetch full key and immediately clear from cache after copying
@@ -89,13 +89,15 @@ export function ApiKeyPill({
exact: true,
})
if (result.isSuccess) return result.data.api_key
if (result.isSuccess) return result.data.api_key ?? ''
if (error) {
toast.error('Failed to copy secret API key')
return ''
}
} catch (error) {
console.error('Failed to fetch API key:', error)
return ''
}
// Fallback to the masked version if fetch fails
@@ -1,4 +1,4 @@
import { ExternalLink, Github, Mail } from 'lucide-react'
import { ExternalLink, Github } from 'lucide-react'
import { LOCAL_STORAGE_KEYS } from 'common'
import { FeatureBanner } from 'components/ui/FeatureBanner'
@@ -119,7 +119,13 @@ export const ApiKeysComingSoonBanner = () => {
</p>
<div className="mt-4">
<Button type="default" icon={<Github />}>
Learn more
<a
href="https://github.com/orgs/supabase/discussions/29260"
target="_blank"
rel="noreferrer"
>
Learn more
</a>
</Button>
</div>
</div>
@@ -4,7 +4,6 @@ import {
useQueryTableCommands,
useSnippetCommands,
} from 'components/layouts/SQLEditorLayout/SqlEditor.Commands'
import { useGenerateSqlCommand } from 'components/interfaces/SqlGenerator/SqlGenerator.Commands'
import { useProjectLevelTableEditorCommands } from 'components/layouts/TableEditorLayout/TableEditor.Commands'
import { useLayoutNavCommands } from 'components/layouts/useLayoutNavCommands'
import { CommandHeader, CommandInput, CommandList, CommandMenu } from 'ui-patterns/CommandMenu'
@@ -19,7 +18,6 @@ import { useSupportCommands } from './Support'
import { orderCommandSectionsByPriority } from './ordering'
export default function StudioCommandMenu() {
useGenerateSqlCommand()
useApiKeysCommands()
useApiUrlCommand()
useProjectLevelTableEditorCommands()
@@ -8,20 +8,6 @@ export const FEATURE_PREVIEWS = [
isNew: true,
isPlatformOnly: false,
},
{
key: LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
name: 'Table Editor Tabs',
discussionsUrl: 'https://github.com/orgs/supabase/discussions/35636',
isNew: true,
isPlatformOnly: false,
},
{
key: LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
name: 'SQL Editor Tabs',
discussionsUrl: 'https://github.com/orgs/supabase/discussions/35636',
isNew: true,
isPlatformOnly: true,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL,
name: 'Project API documentation',
@@ -2,8 +2,6 @@ import { noop } from 'lodash'
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { FeatureFlagContext, LOCAL_STORAGE_KEYS } from 'common'
import { useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import { EMPTY_OBJ } from 'lib/void'
import { FEATURE_PREVIEWS } from './FeaturePreview.constants'
@@ -21,15 +19,10 @@ export const useFeaturePreviewContext = () => useContext(FeaturePreviewContext)
export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}>) => {
const { hasLoaded } = useContext(FeatureFlagContext)
const enableTabsInterface = useFlag('tabsInterface')
// [Joshen] Similar logic to feature flagging previews, we can use flags to default opt in previews
const isDefaultOptIn = (feature: (typeof FEATURE_PREVIEWS)[number]) => {
switch (feature.key) {
case LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS:
return enableTabsInterface
case LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS:
return enableTabsInterface
default:
return false
}
@@ -86,14 +79,3 @@ export const useIsInlineEditorEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR]
}
export const useIsTableEditorTabsEnabled = () => {
const { flags } = useFeaturePreviewContext()
return flags[LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS]
}
export const useIsSQLEditorTabsEnabled = () => {
const { flags } = useFeaturePreviewContext()
if (!IS_PLATFORM) return false
return flags[LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS]
}
@@ -7,22 +7,17 @@ import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { removeTabsByEditor } from 'state/tabs'
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
import { APISidePanelPreview } from './APISidePanelPreview'
import { CLSPreview } from './CLSPreview'
import { FEATURE_PREVIEWS } from './FeaturePreview.constants'
import { useFeaturePreviewContext } from './FeaturePreviewContext'
import { InlineEditorPreview } from './InlineEditorPreview'
import { SqlEditorTabsPreview } from './SqlEditorTabs'
import { TableEditorTabsPreview } from './TableEditorTabs'
const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[key: string]: ReactNode
} = {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_INLINE_EDITOR]: <InlineEditorPreview />,
[LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS]: <TableEditorTabsPreview />,
[LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS]: <SqlEditorTabsPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: <APISidePanelPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: <CLSPreview />,
}
@@ -62,13 +57,6 @@ const FeaturePreviewModal = () => {
properties: { feature: selectedFeatureKey },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
if (ref && selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS) {
removeTabsByEditor(ref, 'table')
}
if (ref && selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS) {
removeTabsByEditor(ref, 'sql')
}
}
function handleCloseFeaturePreviewModal() {
@@ -1,7 +1,6 @@
import Image from 'next/image'
import { BASE_PATH } from 'lib/constants'
import { Admonition } from 'ui-patterns'
export const InlineEditorPreview = () => {
return (
@@ -1,28 +0,0 @@
import { BASE_PATH } from 'lib/constants'
import Image from 'next/image'
export const SqlEditorTabsPreview = () => {
return (
<div className="space-y-2">
<Image
src={`${BASE_PATH}/img/previews/tabs-sql.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm">
The SQL Editor now features tabs for improved navigation and organization. Conveniently go
between the queries that you're focused on, and collapse the sidebar for a bigger real
estate while writing your queries.
</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>Enable opening of queries in the SQL Editor as tabs</li>
<li>Support closing of the navigation sidebar for a larger code editor space</li>
</ul>
</div>
</div>
)
}
@@ -1,192 +0,0 @@
import { ExternalLink, X } from 'lucide-react'
import Image from 'next/image'
import { LOCAL_STORAGE_KEYS } from 'common'
import { useEditorType } from 'components/layouts/editors/EditorsLayout.hooks'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { InlineLinkClassName } from 'components/ui/InlineLink'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { BASE_PATH } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Badge,
Button,
cn,
HoverCard,
HoverCardContent_Shadcn_,
HoverCardTrigger_Shadcn_,
} from 'ui'
import { useIsSQLEditorTabsEnabled, useIsTableEditorTabsEnabled } from './FeaturePreviewContext'
export const TableEditorTabsPreview = () => {
return (
<div className="space-y-2">
<Image
src={`${BASE_PATH}/img/previews/tabs-editor.png`}
width={1860}
height={970}
alt="api-docs-side-panel-preview"
className="rounded border"
/>
<p className="text-foreground-light text-sm">
The Table Editor now features tabs for improved navigation and organization. Have multiple
tables opened across schemas and conveniently go across them without having to switch
schemas. Collapse the sidebar for a bigger real estate while browsing your data.
</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>Enable opening of tables in the Table Editor as tabs</li>
<li>Support closing of the navigation sidebar for a larger data grid space</li>
</ul>
</div>
</div>
)
}
export const TabsUpdateCallout = () => {
const editor = useEditorType()
const appSnap = useAppStateSnapshot()
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
const isTabsEnabled = isTableEditorTabsEnabled || isSQLEditorTabsEnabled
const [tableEditorTabsPreviewState] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
''
)
const [sqlEditorTabsPreviewState] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
''
)
const [tabsInterfaceAcknowledge, setTabsInterfaceAcknowledge] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TABS_INTERFACE_ACKNOWLEDGED,
false
)
const isDefaultOptedInTabs =
isTabsEnabled && tableEditorTabsPreviewState === '' && sqlEditorTabsPreviewState === ''
if (!isDefaultOptedInTabs || tabsInterfaceAcknowledge) return null
return (
<Alert_Shadcn_ className="mb-4 relative">
<AlertTitle_Shadcn_>
<Badge variant="brand" className="mr-2">
NEW
</Badge>
Tabs Interface for Editors
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
The Table and SQL Editors now feature tabs for improved navigation and organization! Check
out our{' '}
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => {
appSnap.setSelectedFeaturePreview(
editor === 'table'
? LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS
: LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS
)
appSnap.setShowFeaturePreviewModal(true)
}}
>
feature previews
</span>{' '}
for more information.
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-4 flex items-center gap-x-2">
<Button asChild type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer noopener"
href="https://github.com/orgs/supabase/discussions/35636"
>
View announcement
</a>
</Button>
</AlertDescription_Shadcn_>
<ButtonTooltip
type="text"
icon={<X />}
className="absolute top-2 right-2 px-1"
onClick={() => setTabsInterfaceAcknowledge(true)}
tooltip={{ content: { side: 'bottom', text: 'Dismiss' } }}
/>
</Alert_Shadcn_>
)
}
export const TabsUpdateTooltip = () => {
const editor = useEditorType()
const appSnap = useAppStateSnapshot()
const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
const isTabsEnabled = isTableEditorTabsEnabled || isSQLEditorTabsEnabled
const [tableEditorTabsPreviewState] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS,
''
)
const [sqlEditorTabsPreviewState] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS,
''
)
const [tabsInterfaceAcknowledge, setTabsInterfaceAcknowledge] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TABS_INTERFACE_ACKNOWLEDGED,
false
)
const isDefaultOptedInTabs =
isTabsEnabled && tableEditorTabsPreviewState === '' && sqlEditorTabsPreviewState === ''
if (!isDefaultOptedInTabs || tabsInterfaceAcknowledge) return null
return (
<HoverCard openDelay={150}>
<HoverCardTrigger_Shadcn_ asChild>
<div
className={cn(
'flex items-center justify-center px-2 h-10 shrink-0',
'border-b border-b-default gap-x-2'
)}
>
<Badge variant="brand" className="rounded">
<span>NEW</span>
</Badge>
</div>
</HoverCardTrigger_Shadcn_>
<HoverCardContent_Shadcn_ className="p-3 w-72">
<div className="flex flex-col gap-y-1">
<p className="text-sm">Tabs Interface for Editors</p>
<p className="text-xs text-foreground-light">
The Table and SQL Editors now feature tabs for improved navigation and organization!
Check out our{' '}
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => {
appSnap.setSelectedFeaturePreview(
editor === 'table'
? LOCAL_STORAGE_KEYS.UI_TABLE_EDITOR_TABS
: LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS
)
appSnap.setShowFeaturePreviewModal(true)
}}
>
feature previews
</span>{' '}
for more information.
</p>
</div>
<Button type="default" className="mt-2" onClick={() => setTabsInterfaceAcknowledge(true)}>
Dismiss
</Button>
</HoverCardContent_Shadcn_>
</HoverCard>
)
}
@@ -2,7 +2,7 @@ import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { toast } from 'sonner'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useParams } from 'common'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import useLatest from 'hooks/misc/useLatest'
@@ -18,6 +18,7 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
const isLoggedIn = useIsLoggedIn()
const snap = useAppStateSnapshot()
const isUserMFAEnabled = useIsMFAEnabled()
const organization = useSelectedOrganization()
@@ -122,7 +123,17 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
}, [isSuccessStorage, ref])
useEffect(() => {
if (organization) setLastVisitedOrganization(organization.slug)
if (organization) {
setLastVisitedOrganization(organization.slug)
if (
organization.organization_requires_mfa &&
!isUserMFAEnabled &&
router.pathname !== '/org/[slug]'
) {
router.push(`/org/${organization.slug}`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organization])
@@ -119,7 +119,7 @@ export const UsersV2 = () => {
}
)
const { data: countData } = useUsersCountQuery({
const { data: countData, refetch: refetchCount } = useUsersCountQuery({
projectRef,
connectionString: project?.connectionString,
keywords: filterKeywords,
@@ -437,7 +437,10 @@ export const UsersV2 = () => {
icon={<RefreshCw />}
type="default"
loading={isRefetching && !isFetchingNextPage}
onClick={() => refetch()}
onClick={() => {
refetch()
refetchCount()
}}
>
Refresh
</Button>
@@ -1,18 +1,44 @@
export const USAGE_APPROACHING_THRESHOLD = 0.8
export const CANCELLATION_REASONS = [
'Pricing',
"My project isn't getting traction",
'Poor customer service',
'Missing feature',
"I didn't see the value",
"Supabase didn't meet my needs",
'Dashboard is too complicated',
'Postgres is too complicated',
'Problem not solved',
'Too many bugs/issues',
'I decided to use something else',
'My work has finished/discontinued',
'I’m migrating to/starting a new project',
'None of the above',
{
value: 'I was just exploring, or it was a hobby/student project.',
},
{
value: 'I was not satisfied with the customer support I received.',
label: 'Could you tell us more about your experience with our support team?',
},
{
value: 'Supabase is missing a specific feature I need.',
label: 'What specific feature(s) are we missing?',
},
{
value: 'I found it difficult to use or build with.',
label: 'What specific parts of Supabase did you find difficult or frustrating?',
},
{
value: 'Performance or reliability insufficient.',
label:
'Could you tell us more about the specific issues you encountered (e.g., UI bugs, API latency, downtime)?',
},
{
value: 'My project was cancelled or put on hold.',
},
{
value: 'Too expensive',
label: 'We appreciate your perspective on our pricing, what aspects of the cost felt too high?',
},
{
value: 'The pricing is unpredictable and hard to budget for.',
label:
'Which aspects of our pricing model made it difficult for you to predict your monthly costs?',
},
{
value: 'My company went out of business or was acquired.',
},
{
value: 'I lost trust in the company or its future direction.',
label:
'Building and maintaining your trust is our highest priority, could you please share the specific event or reason that led to this loss of trust?',
},
]
@@ -118,7 +118,7 @@ const AddPaymentMethodForm = ({
<PaymentElement
className="[.p-LinkAutofillPrompt]:pt-0"
options={{
defaultValues: { billingDetails: { email: selectedOrganization?.billing_email } },
defaultValues: { billingDetails: { email: selectedOrganization?.billing_email ?? '' } },
}}
/>
{showSetDefaultCheckbox && (
@@ -7,11 +7,9 @@ export const PaymentConfirmation = ({
paymentIntentSecret,
onPaymentIntentConfirm,
onLoadingChange,
paymentMethodId,
onError,
}: {
paymentIntentSecret: string
paymentMethodId: string
onPaymentIntentConfirm: (response: PaymentIntentResult) => void
onLoadingChange: (loading: boolean) => void
onError?: (error: Error) => void
@@ -22,7 +20,7 @@ export const PaymentConfirmation = ({
if (stripe && paymentIntentSecret) {
onLoadingChange(true)
stripe!
.confirmCardPayment(paymentIntentSecret, { payment_method: paymentMethodId })
.confirmCardPayment(paymentIntentSecret)
.then((res) => {
onPaymentIntentConfirm(res)
onLoadingChange(false)
@@ -2,11 +2,12 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ExternalLink, Plug } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import Panel from 'components/ui/Panel'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
@@ -137,13 +138,21 @@ export const Connect = () => {
return []
}
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : ''
const apiUrl = canReadAPIKeys ? apiHost : null
const { anonKey } = canReadAPIKeys ? getAPIKeys(settings) : { anonKey: null }
const projectKeys = { apiUrl, anonKey: anonKey?.api_key ?? null }
const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: false })
const projectKeys = useMemo(() => {
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : ''
const apiUrl = canReadAPIKeys ? apiHost : null
return {
apiUrl: apiHost ?? null,
anonKey: anonKey?.api_key ?? null,
publishableKey: apiKeys?.find(({ type }) => type === 'publishable')?.api_key ?? null,
}
}, [apiKeys, anonKey, canReadAPIKeys, settings])
const filePath = getContentFilePath({
connectionObject,
@@ -1,12 +1,14 @@
export type projectKeys = {
apiUrl: string | null
anonKey: string | null
publishableKey: string | null
}
export interface ContentFileProps {
projectKeys: {
apiUrl: string
anonKey: string
anonKey?: string
publishableKey?: string
}
connectionStringPooler: {
transactionShared: string
@@ -239,7 +239,7 @@ export const DatabaseConnectionString = () => {
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<DatabaseSelector buttonProps={{ size: 'small' }} />
<DatabaseSelector portal={false} buttonProps={{ size: 'small' }} />
</div>
{isLoading && (
@@ -30,7 +30,7 @@ data class TodoItem(val id: Int, val name: String)
{`
val supabase = createSupabaseClient(
supabaseUrl = "${projectKeys.apiUrl ?? 'your-project-url'}",
supabaseKey = "${projectKeys.anonKey ?? 'your-anon-key'}"
supabaseKey = "${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile apps>'}"
) {
install(Postgrest)
}
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
EXPO_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
EXPO_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
EXPO_PUBLIC_SUPABASE_KEY=${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile and desktop apps>'}
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -31,17 +31,18 @@ EXPO_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
{`
import 'react-native-url-polyfill/auto'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'
import { createClient, processLock } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL || "",
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || "",
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_KEY!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
lock: processLock,
},
})
`}
@@ -24,7 +24,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
await Supabase.initialize(
url: '${projectKeys.apiUrl ?? 'your-project-url'}',
anonKey: '${projectKeys.anonKey ?? 'your-anon-key'}',
anonKey: '${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile and desktop apps>'}',
);
runApp(MyApp());
}
@@ -24,7 +24,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
{`
export const environment = {
supabaseUrl: '${projectKeys.apiUrl ?? 'your-project-url'}',
supabaseKey: '${projectKeys.anonKey ?? 'your-anon-key'}',
supabaseKey: '${projectKeys.publishableKey ?? '<prefer publishabke key instead of anon key for mobile apps>'}',
};
`}
</SimpleCodeBlock>
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
REACT_APP_SUPABASE_KEY=${projectKeys.publishableKey ?? '<prefer publishable key instead of anon key for mobile or desktop apps>'}
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -32,7 +32,7 @@ REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY
const supabaseKey = process.env.REACT_APP_SUPABASE_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
`}
@@ -21,10 +21,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env.local">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -58,10 +62,13 @@ export default async function Page() {
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
supabaseUrl!,
supabaseKey!,
{
cookies: {
getAll() {
@@ -88,10 +95,13 @@ export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
{`
import { createBrowserClient } from "@supabase/ssr";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const createClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
supabaseUrl!,
supabaseKey!,
);
`}
</SimpleCodeBlock>
@@ -103,6 +113,9 @@ export const createClient = () =>
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const createClient = (request: NextRequest) => {
// Create an unmodified response
let supabaseResponse = NextResponse.next({
@@ -112,8 +125,8 @@ export const createClient = (request: NextRequest) => {
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
supabaseUrl!,
supabaseKey!,
{
cookies: {
getAll() {
@@ -134,7 +147,6 @@ export const createClient = (request: NextRequest) => {
return supabaseResponse
};
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -19,10 +19,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env.local">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`NEXT_PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -32,7 +36,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const supabaseKey = process.env.${projectKeys?.publishableKey ? 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'NEXT_PUBLIC_SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
`}
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -19,10 +19,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env.local">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`REACT_APP_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -32,7 +36,7 @@ REACT_APP_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
const supabaseKey = process.env.REACT_APP_SUPABASE_ANON_KEY;
const supabaseKey = process.env.${projectKeys.publishableKey ? 'REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'REACT_APP_SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
`}
@@ -19,10 +19,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`VITE_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -32,7 +36,8 @@ VITE_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
const supabaseKey = import.meta.env.${projectKeys.publishableKey ? 'VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'VITE_SUPABASE_ANON_KEY'};
const supabase = createClient(supabaseUrl, supabaseKey);
export default supabase
@@ -19,10 +19,12 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env.local">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
`SUPABASE_KEY=${projectKeys?.publishableKey ?? projectKeys?.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -32,7 +34,7 @@ SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from "@refinedev/supabase";
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_KEY
const SUPABASE_KEY = process.env.SUPABASE_KEY;
export const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY, {
db: {
@@ -19,10 +19,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -35,9 +39,12 @@ export function createClient(request: Request) {
const cookies = parse(request.headers.get("Cookie") ?? "");
const headers = new Headers();
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'SUPABASE_ANON_KEY'};
return createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
supabaseUrl!,
supabaseKey!,
{
cookies: {
get(key) {
@@ -19,10 +19,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env.local">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -32,9 +36,9 @@ SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_ANON_KEY;
const supabaseKey = process.env.${projectKeys.publishableKey ? 'SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
export const supabase = createClient(supabaseUrl!, supabaseKey!);
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -20,10 +20,14 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<ConnectTabContent value=".env.local">
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
`}
{[
'',
`PUBLIC_SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}`,
projectKeys?.publishableKey
? `PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${projectKeys.publishableKey}`
: `PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}`,
'',
].join('\n')}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -31,10 +35,10 @@ PUBLIC_SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
<SimpleCodeBlock className="js" parentClassName="min-h-72">
{`
import { createClient } from "@supabase/supabase-js";
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from "$env/static/public"
import { PUBLIC_SUPABASE_URL, ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'} } from "$env/static/public"
const supabaseUrl = PUBLIC_SUPABASE_URL;
const supabaseKey = PUBLIC_SUPABASE_ANON_KEY;
const supabaseKey = ${projectKeys.publishableKey ? 'PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY' : 'PUBLIC_SUPABASE_ANON_KEY'};
export const supabase = createClient(supabaseUrl, supabaseKey);
`}
@@ -25,7 +25,7 @@ import Supabase
let supabase = SupabaseClient(
supabaseURL: URL(string: "${projectKeys.apiUrl ?? 'your-project-url'}")!,
supabaseKey: "${projectKeys.anonKey ?? 'your-anon-key'}"
supabaseKey: "${projectKeys.publishableKey ?? '<prefer publishable key for native apps instead of anon key>'}"
)
`}
</SimpleCodeBlock>
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
{`
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
SUPABASE_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
SUPABASE_KEY=${projectKeys.publishableKey ?? projectKeys.anonKey ?? 'your-anon-key'}
`}
</SimpleCodeBlock>
</ConnectTabContent>
@@ -134,6 +134,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
useEffect(() => {
if (visible) {
setFocusedEditor(false)
form.reset({
name: func?.name ?? '',
schema: func?.schema ?? 'public',
@@ -77,6 +77,7 @@ const Indexes = () => {
projectRef: project.ref,
connectionString: project.connectionString,
name: index.name,
schema: selectedSchema,
})
}
@@ -53,9 +53,14 @@ import { IOPSField } from './fields/IOPSField'
import { StorageTypeField } from './fields/StorageTypeField'
import { ThroughputField } from './fields/ThroughputField'
import { DiskCountdownRadial } from './ui/DiskCountdownRadial'
import { DiskType, RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3 } from './ui/DiskManagement.constants'
import {
DISK_LIMITS,
DiskType,
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3,
} from './ui/DiskManagement.constants'
import { NoticeBar } from './ui/NoticeBar'
import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
import { useIsAwsCloudProvider, useIsAwsK8sCloudProvider } from 'hooks/misc/useSelectedProject'
export function DiskManagementForm() {
// isLoading is used to avoid a useCheckPermissions() race condition
@@ -69,7 +74,8 @@ export function DiskManagementForm() {
(warning) => warning.project === project?.ref
)
const isReadOnlyMode = projectResourceWarnings?.is_readonly_mode_enabled
const isFlyArchitecture = project?.cloud_provider === 'FLY'
const isAws = useIsAwsCloudProvider()
const isAwsK8s = useIsAwsK8sCloudProvider()
/**
* Permissions
@@ -118,31 +124,32 @@ export function DiskManagementForm() {
setRefetchInterval(2000)
}
},
enabled: project != null && !isFlyArchitecture,
enabled: project != null && isAws,
}
)
const { isSuccess: isAddonsSuccess } = useProjectAddonsQuery({ projectRef })
const { isWithinCooldownWindow, isSuccess: isCooldownSuccess } =
useRemainingDurationForDiskAttributeUpdate({
projectRef,
enabled: project != null && !isFlyArchitecture,
enabled: project != null && isAws,
})
const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery(
{
projectRef,
},
{ enabled: project != null && !isFlyArchitecture }
{ enabled: project != null && isAws }
)
const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } =
useDiskAutoscaleCustomConfigQuery(
{ projectRef },
{ enabled: project != null && !isFlyArchitecture }
)
useDiskAutoscaleCustomConfigQuery({ projectRef }, { enabled: project != null && isAws })
/**
* Handle default values
*/
const computeSize = project?.infra_compute_size
? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size)
: undefined
// @ts-ignore
const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0, iops: 0 }
const { growth_percent, max_size_gb, min_increment_gb } = diskAutoscaleConfig ?? {}
@@ -151,9 +158,7 @@ export function DiskManagementForm() {
provisionedIOPS: iops,
throughput: throughput_mbps,
totalSize: size_gb,
computeSize: project?.infra_compute_size
? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size)
: undefined,
computeSize,
growthPercent: growth_percent,
minIncrementGb: min_increment_gb,
maxSizeGb: max_size_gb,
@@ -168,6 +173,20 @@ export function DiskManagementForm() {
reValidateMode: 'onChange',
})
const { computeSize: modifiedComputeSize } = form.watch()
// We only support disk configurations for >=Large instances
// If a customer downgrades back to <Large, we should reset the storage settings to avoid incurring unnecessary costs
useEffect(() => {
if (modifiedComputeSize && project?.infra_compute_size && isDialogOpen) {
if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)) {
form.setValue('storageType', DiskType.GP3)
form.setValue('throughput', DISK_LIMITS['gp3'].minThroughput)
form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops)
}
}
}, [modifiedComputeSize, isDialogOpen, project])
/**
* State handling
*/
@@ -200,7 +219,7 @@ export function DiskManagementForm() {
isPlanUpgradeRequired ||
isWithinCooldownWindow ||
!canUpdateDiskConfiguration ||
isFlyArchitecture
!isAws
const disableComputeInputs = isPlanUpgradeRequired
const isDirty = !!Object.keys(form.formState.dirtyFields).length
@@ -344,15 +363,17 @@ export function DiskManagementForm() {
<SpendCapDisabledSection />
<NoticeBar
type="default"
visible={isFlyArchitecture}
title="Disk configuration is not available on Fly Postgres"
visible={!isAws}
title="Disk configuration is only available for projects in the AWS cloud provider"
description={
isBranch
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
: 'The Fly Postgres offering is deprecated - please migrate your instance to Supabase to configure your disk.'
isAwsK8s
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
: isBranch
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
}
/>
{!isFlyArchitecture && (
{isAws && (
<>
<div className="flex flex-col gap-y-3">
<DiskCountdownRadial />
@@ -13,11 +13,7 @@ import {
} from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import { ComputeSizeRecommendationSection } from '../ui/ComputeSizeRecommendationSection'
import {
COMPUTE_BASELINE_IOPS,
DiskType,
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3,
} from '../ui/DiskManagement.constants'
import { DiskType, RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 } from '../ui/DiskManagement.constants'
import { DiskManagementIOPSReadReplicas } from '../ui/DiskManagementReadReplicas'
import FormMessage from '../ui/FormMessage'
import { InputPostTab } from '../ui/InputPostTab'
@@ -117,13 +113,7 @@ export function IOPSField({ form, disableInput }: IOPSFieldProps) {
type="number"
className="flex-grow font-mono rounded-r-none max-w-32"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_IOPS[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_IOPS
]
: field.value
}
value={field.value}
disabled={disableInput || disableIopsInput || isError}
onChange={(e) => {
setValue('provisionedIOPS', e.target.valueAsNumber, {
@@ -11,7 +11,6 @@ import { DiskStorageSchemaType } from '../DiskManagement.schema'
import { calculateThroughputPrice } from '../DiskManagement.utils'
import { BillingChangeBadge } from '../ui/BillingChangeBadge'
import {
COMPUTE_BASELINE_THROUGHPUT,
DISK_LIMITS,
DiskType,
RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3,
@@ -129,13 +128,7 @@ export function ThroughputField({ form, disableInput }: ThroughputFieldProps) {
<Input_Shadcn_
type="number"
{...field}
value={
disableIopsInput
? COMPUTE_BASELINE_THROUGHPUT[
watchedComputeSize as keyof typeof COMPUTE_BASELINE_THROUGHPUT
]
: field.value
}
value={field.value}
onChange={(e) => {
setValue('throughput', e.target.valueAsNumber, {
shouldDirty: true,
@@ -1,15 +1,18 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Key } from 'lucide-react'
import { useMemo } from 'react'
import { useParams } from 'common'
import type { showApiKey } from 'components/interfaces/Docs/Docs.types'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
@@ -17,25 +20,30 @@ const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
interface LangSelectorProps {
selectedLang: string
showApiKey: showApiKey
selectedApiKey: showApiKey
setSelectedLang: (selectedLang: string) => void
setShowApiKey: (showApiKey: showApiKey) => void
setSelectedApiKey: (showApiKey: showApiKey) => void
}
const LangSelector = ({
selectedLang,
showApiKey,
selectedApiKey,
setSelectedLang,
setShowApiKey,
setSelectedApiKey,
}: LangSelectorProps) => {
const { ref: projectRef } = useParams()
const canReadServiceKey = useCheckPermissions(
PermissionAction.READ,
'service_api_keys.service_role_key'
)
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { anonKey: anonApiKey, serviceKey: serviceApiKey } = getAPIKeys(settings)
const { data: apiKeys = [], isLoading: isLoadingAPIKeys } = useAPIKeysQuery({
projectRef,
reveal: false,
})
const legacyKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'legacy'), [apiKeys])
const publishableKeys = useMemo(
() => apiKeys.filter(({ type }) => type === 'publishable'),
[apiKeys]
)
const secretKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'secret'), [apiKeys])
return (
<div className="p-1 w-1/2 ml-auto">
@@ -62,48 +70,93 @@ const LangSelector = ({
>
Bash
</button>
{selectedLang == 'bash' && (
<div className="flex">
{selectedLang == 'bash' && !isLoadingAPIKeys && apiKeys && apiKeys.length > 0 && (
<div className="flex gap-x-1">
<div className="flex items-center gap-2 p-1 pl-2 text-xs text-foreground-lighter">
<Key size={12} strokeWidth={1.5} />
<span>Project API key :</span>
<span>Project API key:</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default">{showApiKey.name}</Button>
<Button type="outline">
{selectedApiKey.name === 'hide' ? 'Hide keys' : selectedApiKey.name}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<>
<DropdownMenuItem key="hide" onClick={() => setShowApiKey(DEFAULT_KEY)}>
hide
</DropdownMenuItem>
{anonApiKey && (
<DropdownMenuItem
key="anon"
onClick={() =>
setShowApiKey({
key: anonApiKey.api_key ?? '-',
name: 'anon (public)',
})
}
>
<p>anon (public)</p>
</DropdownMenuItem>
<DropdownMenuRadioGroup value={selectedApiKey.key}>
<DropdownMenuRadioItem
key="hide"
value={DEFAULT_KEY.key}
onClick={() => setSelectedApiKey(DEFAULT_KEY)}
>
Hide keys
</DropdownMenuRadioItem>
{publishableKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Publishable keys</DropdownMenuLabel>
{publishableKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({
name: `Publishable key: ${key.name}`,
key: value,
})
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
{canReadServiceKey && (
<DropdownMenuItem
key="service"
onClick={() =>
setShowApiKey({
key: serviceApiKey?.api_key ?? '-',
name: 'service_role (secret)',
})
}
>
<p>service_role (secret)</p>
</DropdownMenuItem>
{secretKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Secret keys</DropdownMenuLabel>
{secretKeys.map((key) => {
const value = key.prefix + '...'
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
</>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>JWT-based legacy keys</DropdownMenuLabel>
{legacyKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Legacy key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</DropdownMenuGroup>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -15,7 +15,10 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import { prettifyJSON } from 'lib/helpers'
import { getRoleImpersonationJWT } from 'lib/role-impersonation'
import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
import {
RoleImpersonationStateContextProvider,
useGetImpersonatedRoleState,
} from 'state/role-impersonation-state'
import {
Badge,
Button,
@@ -159,7 +162,7 @@ export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTester
// Validate that the body is valid JSON
try {
JSON.parse(values.body)
JSON.parse(JSON.stringify(values.body))
} catch (e) {
form.setError('body', { message: 'Must be a valid JSON string' })
return
@@ -416,7 +419,12 @@ export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTester
<SheetFooter className="px-5 py-3 border-t">
<div className="flex items-center gap-2">
<RoleImpersonationPopover portal={false} />
{/* [Alaister]: We're using a fresh context here as edge functions don't allow impersonating users. */}
<RoleImpersonationStateContextProvider
key={`role-impersonation-state-${projectRef}`}
>
<RoleImpersonationPopover portal={false} disallowAuthenticatedOption={true} />
</RoleImpersonationStateContextProvider>
<Button
type="primary"
htmlType="submit"
@@ -7,7 +7,9 @@ import z from 'zod'
import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import { useSecretsCreateMutation } from 'data/secrets/secrets-create-mutation'
import { useSecretsQuery } from 'data/secrets/secrets-query'
import { Eye, EyeOff, MinusCircle } from 'lucide-react'
import { DuplicateSecretWarningModal } from './DuplicateSecretWarningModal'
import {
Button,
Form_Shadcn_,
@@ -44,6 +46,8 @@ const defaultValues = {
const AddNewSecretForm = () => {
const { ref: projectRef } = useParams()
const [showSecretValue, setShowSecretValue] = useState(false)
const [duplicateSecretName, setDuplicateSecretName] = useState<string>('')
const [pendingSecrets, setPendingSecrets] = useState<z.infer<typeof FormSchema> | null>(null)
const form = useForm({
resolver: zodResolver(FormSchema),
@@ -55,6 +59,10 @@ const AddNewSecretForm = () => {
name: 'secrets',
})
const { data: existingSecrets } = useSecretsQuery({
projectRef: projectRef,
})
function handlePaste(e: ClipboardEvent) {
e.preventDefault()
const text = e.clipboardData?.getData('text')
@@ -114,9 +122,32 @@ const AddNewSecretForm = () => {
})
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => {
// Check for duplicate secret names
const existingSecretNames = existingSecrets?.map((secret) => secret.name) || []
const duplicateSecret = data.secrets.find((secret) => existingSecretNames.includes(secret.name))
if (duplicateSecret) {
setDuplicateSecretName(duplicateSecret.name)
setPendingSecrets(data)
return
}
createSecret({ projectRef, secrets: data.secrets })
}
const handleConfirmDuplicate = () => {
if (pendingSecrets) {
createSecret({ projectRef, secrets: pendingSecrets.secrets })
setDuplicateSecretName('')
setPendingSecrets(null)
}
}
const handleCancelDuplicate = () => {
setDuplicateSecretName('')
setPendingSecrets(null)
}
return (
<Panel>
<Panel.Content className="grid gap-4">
@@ -202,6 +233,14 @@ const AddNewSecretForm = () => {
</form>
</Form_Shadcn_>
</Panel.Content>
<DuplicateSecretWarningModal
visible={!!duplicateSecretName}
onCancel={handleCancelDuplicate}
onConfirm={handleConfirmDuplicate}
isCreating={isCreating}
secretName={duplicateSecretName}
/>
</Panel>
)
}
@@ -0,0 +1,36 @@
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
interface DuplicateSecretWarningModalProps {
visible: boolean
onCancel: () => void
onConfirm: () => void
isCreating: boolean
secretName: string
}
export const DuplicateSecretWarningModal = ({
visible,
onCancel,
onConfirm,
isCreating,
secretName,
}: DuplicateSecretWarningModalProps) => {
return (
<ConfirmationModal
visible={visible}
size="medium"
title="Confirm replacing existing secret"
confirmLabel="Replace secret"
confirmLabelLoading="Replacing secret"
variant="warning"
loading={isCreating}
onCancel={onCancel}
onConfirm={onConfirm}
>
<p className="text-sm text-foreground-light">
A secret with the name "{secretName}" already exists. Continuing will replace the existing
secret with the new value. This action cannot be undone. Are you sure you want to proceed?
</p>
</ConfirmationModal>
)
}
@@ -42,6 +42,18 @@ const EdgeFunctionSecrets = () => {
[]
: data ?? []
const headers = [
<Table.th key="secret-name">Name</Table.th>,
<Table.th key="secret-value" className="flex items-center gap-x-2">
Digest{' '}
<Badge color="scale" className="font-mono">
SHA256
</Badge>
</Table.th>,
<Table.th key="secret-updated-at">Updated at</Table.th>,
<Table.th key="actions" />,
]
return (
<>
{isLoading && <GenericSkeletonLoader />}
@@ -73,17 +85,7 @@ const EdgeFunctionSecrets = () => {
<div className="w-full overflow-hidden overflow-x-auto">
<Table
head={[
<Table.th key="secret-name">Name</Table.th>,
<Table.th key="secret-value" className="flex items-center gap-x-2">
Digest{' '}
<Badge color="scale" className="font-mono">
SHA256
</Badge>
</Table.th>,
<Table.th key="secret-updated-at">Updated at</Table.th>,
<Table.th key="actions" />,
]}
head={headers}
body={
secrets.length > 0 ? (
secrets.map((secret) => (
@@ -95,7 +97,7 @@ const EdgeFunctionSecrets = () => {
))
) : secrets.length === 0 && searchString.length > 0 ? (
<Table.tr>
<Table.td colSpan={3}>
<Table.td colSpan={headers.length}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{searchString}" did not return any results
@@ -104,7 +106,7 @@ const EdgeFunctionSecrets = () => {
</Table.tr>
) : (
<Table.tr>
<Table.td colSpan={3}>
<Table.td colSpan={headers.length}>
<p className="text-sm text-foreground">No secrets created</p>
<p className="text-sm text-foreground-light">
There are no secrets associated with your project yet
@@ -63,7 +63,7 @@ export const EXAMPLE_PROJECTS = [
framework: 'nextjs',
title: 'Next.js Subscription and Auth',
description: 'The all-in-one starter kit for high-performance SaaS applications.',
url: 'https://github.com/vercel/nextjs-subscription-payments',
url: 'https://github.com/nextjs/saas-starter',
type: 'app',
},
{
@@ -3,11 +3,10 @@ import { useEffect, useState } from 'react'
import { UseFormReturn } from 'react-hook-form'
import { useDebounce } from 'use-debounce'
import { useCompletion } from 'ai/react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useSqlCronGenerateMutation } from 'data/ai/sql-cron-mutation'
import { useCronTimezoneQuery } from 'data/database-cron-jobs/database-cron-timezone-query'
import { constructHeaders } from 'data/fetchers'
import { BASE_PATH } from 'lib/constants'
import { useFlag } from 'hooks/ui/useFlag'
import {
Accordion_Shadcn_,
AccordionContent_Shadcn_,
@@ -37,6 +36,7 @@ interface CronJobScheduleSectionProps {
export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobScheduleSectionProps) => {
const { project } = useProjectContext()
const useBedrockAssistant = useFlag('useBedrockAssistant')
const [inputValue, setInputValue] = useState('')
const [debouncedValue] = useDebounce(inputValue, 750)
const [useNaturalLanguage, setUseNaturalLanguage] = useState(false)
@@ -50,25 +50,13 @@ export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobSchedul
{ name: 'Every Monday at 2 AM', expression: '0 2 * * 1' },
] as const
const {
complete: generateCronSyntax,
isLoading: isGeneratingCron,
stop,
} = useCompletion({
api: `${BASE_PATH}/api/ai/sql/cron`,
onResponse: async (response) => {
if (response.ok) {
// remove quotes from the cron expression
const expression = (await response.text()).trim().replace(/^"|"$/g, '')
form.setValue('schedule', expression, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
})
}
},
onError: (error) => {
console.error('Error generating cron:', error)
const { mutate: generateCronSyntax, isLoading: isGeneratingCron } = useSqlCronGenerateMutation({
onSuccess: (expression) => {
form.setValue('schedule', expression, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
})
},
})
@@ -79,12 +67,7 @@ export const CronJobScheduleSection = ({ form, supportsSeconds }: CronJobSchedul
useEffect(() => {
if (useNaturalLanguage && debouncedValue) {
constructHeaders().then((headers) =>
generateCronSyntax(debouncedValue, {
headers: { Authorization: headers.get('Authorization') ?? '' },
})
)
return () => stop()
generateCronSyntax({ prompt: debouncedValue, useBedrockAssistant })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedValue, useNaturalLanguage])
@@ -0,0 +1,97 @@
export interface AlgorithmDetail {
name: string
description: string
pros: string[]
cons: string[]
label: string
shortDescription: string
links: { url: string; label: string }[]
}
export const algorithmDetails: Record<string, AlgorithmDetail> = {
HS256: {
label: 'HS256 (Symmetric)',
name: 'HMAC with SHA-256',
description: 'Symmetric algorithm using a shared secret key',
pros: [
'Fast and simple to use',
'Requires less computational power',
'Suitable for server-to-server communication',
],
cons: [
'Requires secure key exchange',
"Not suitable when the verifier shouldn't be able to sign tokens",
'Key needs to be kept secret on both sides',
],
shortDescription: 'HMAC with SHA-256: Fast, simple, requires secure key exchange',
links: [
{ url: 'https://jwt.io/introduction', label: 'JWT.io Introduction' },
{
url: 'https://datatracker.ietf.org/doc/html/rfc7518#section-3.2',
label: 'RFC 7518 Specification',
},
],
},
RS256: {
label: 'RSA 2048',
name: 'RSA with SHA-256',
description: 'Asymmetric algorithm using a public/private key pair',
pros: [
'Allows public key to be distributed freely',
'Private key can be kept secret on the signing side',
"Suitable for scenarios where the verifier shouldn't be able to sign tokens",
],
cons: [
'Slower than HS256',
'Requires more computational power',
'Keys are larger than ECDSA keys',
],
shortDescription: 'RSA with SHA-256: Allows public key distribution, slower',
links: [
{ url: 'https://jwt.io/introduction', label: 'JWT.io Introduction' },
{
url: 'https://datatracker.ietf.org/doc/html/rfc7518#section-3.3',
label: 'RFC 7518 Specification',
},
],
},
ES256: {
label: 'ECC (P-256)',
name: 'ECDSA with SHA-256',
description: 'Asymmetric algorithm using elliptic curve cryptography',
pros: [
'Faster than RSA',
'Smaller key and signature sizes compared to RSA',
'Provides forward secrecy',
],
cons: [
'Less widely supported than RSA',
'More complex to implement correctly',
'Requires careful implementation to avoid timing attacks',
],
shortDescription: 'ECDSA with SHA-256: Compact keys, fast, modern alternative to RSA',
links: [
{ url: 'https://jwt.io/introduction', label: 'JWT.io Introduction' },
{
url: 'https://datatracker.ietf.org/doc/html/rfc7518#section-3.4',
label: 'RFC 7518 Specification',
},
],
},
}
export const algorithmLabels = Object.keys(algorithmDetails).reduce(
(a, i) => {
a[i] = algorithmDetails[i].label
return a
},
{} as { [name: keyof typeof algorithmDetails]: string }
)
export const algorithmDescriptions = Object.keys(algorithmDetails).reduce(
(a, i) => {
a[i] = algorithmDetails[i].shortDescription
return a
},
{} as { [name: keyof typeof algorithmDetails]: string }
)
@@ -0,0 +1,24 @@
import { GlobeLock } from 'lucide-react'
import React from 'react'
import { InfoPill } from '../../ui/InfoPill'
import { AlgorithmDetail, algorithmDetails } from './algorithm-details'
interface AlgorithmHoverCardProps {
algorithm: keyof typeof algorithmDetails
legacy?: boolean
}
export const AlgorithmHoverCard: React.FC<AlgorithmHoverCardProps> = ({ algorithm, legacy }) => {
const details: AlgorithmDetail = algorithmDetails[algorithm]
return (
<InfoPill
label={legacy ? `Legacy JWT Secret ${details.label}` : details.label}
icon={<GlobeLock className="w-4" strokeWidth={1} />}
title={details.name}
description={details.description}
links={details.links}
/>
)
}
Loaded 100 of 460 files, more files were not shown because too many files have changed in this diff. Show more