Merge branch 'master' into docs/paas

This commit is contained in:
Wen Bo Xie authored and GitHub committed 2025-11-26 20:31:26 +08:00
commit 9f21f06f91
206 files changed
+4387 -1915

No files matched your search

+6
View File
@@ -124,6 +124,12 @@ jobs:
docker buildx imagetools create -t supabase/studio:latest \
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
echo "Published Registry Images" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Image | Link |" >> $GITHUB_STEP_SUMMARY
echo "|-------|------|" >> $GITHUB_STEP_SUMMARY
echo "| \`supabase/studio:${{ needs.settings.outputs.image_version }}\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=${{ needs.settings.outputs.image_version }}) |" >> $GITHUB_STEP_SUMMARY
echo "| \`supabase/studio:latest\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=latest) |" >> $GITHUB_STEP_SUMMARY
publish:
needs:
@@ -21,29 +21,88 @@ About to push some code? At a minimum, check your work against this list:
## Focus management
All interactive page elements should be reachable by keyboard. They should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly.
All interactive page elements should be reachable by keyboard. Given the below inconsistency between devices and browsers, add `tabIndex={0}` to all buttons, links, and non-text inputs, ideally at the component level. Consider tying the state of `tabIndex` to the `disabled` state of a component, if applicable.
```jsx
<TableCell>
<p>{name}</p>
<button
className={cn('absolute inset-0', 'inset-focus')}
onClick={(event) => handleBucketNavigation(name, event)}
>
<span className="sr-only">Go to bucket details</span>
</button>
</TableCell>
Chromium-based browsers and Firefox handle this automatically via the Tab key. Safari, by default, requires the Option key to also be held down. Enabling _Keyboard navigation_ on macOS Settings [removes this requirement](https://mayank.co/blog/safari-focus/#keyboard-navigation) but makes links non-tabbable as a result.
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly.
[Button](../components/button) has all of the above built-in. Bespoke interactive elements however, such as the below clickable [TableRow](../components/table), require these props to be added manually:
```tsx showLineNumbers {3, 5-11}
<TableRow
key={id}
className="relative cursor-pointer h-16 inset-focus"
onClick={(event) => handleBucketNavigation(name, event)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
handleBucketNavigation(name, event)
}
}}
tabIndex={0}
>
<TableCell>
<p>{name}</p>
</TableCell>
</TableRow>
```
Consider also affordances like `ctrl` and `meta` key support for opening in a new tab. Anything that you can do with a mouse input should be replicable by keyboard.
## Screen reader support
### Radio groups
Textual elements are supported out-of-the-box by screen readers. Imagery of course should be described by `alt` tags.
Single-select option groups such as `<input type="radio">` should behave as a single control. Only the first item of radio groups should become focused with the Tab key. The next Tab should move focus from the group to the next focusable control.
Less obvious however are scaffolding elements that only makes sense visually, when paired with other content. For example: a table column for actions may not have a visual _Actions_ label because its purpose is obvious to a sighted person. For everyone else’s sake, this column should be titled with `sr-only` text:
Individual options inside of a group can be reached by arrow keys (↑ ↓ ← →). Space is the canonical key to activate radio options, with Enter being a secondary affordance.
```jsx
### Jumping ahead
Some keyboard-navigable content may be contain hundreds or thousands of items. Help users jump to specific content with the following mitigation strategies:
- Search and filtering
- Pagination or virtualization
- “Jump to” shortcuts to skip ahead
## Screen readers
Textual elements are supported out-of-the-box by screen readers.
### Imagery
Images should have their contents described with an `alt` attribute. Write an objective description of the content rather than its context. For example:
```tsx showLineNumbers {2}
// Correct: painting a picture with words
<img src="beagle.png" alt="A tricolor beagle galloping through a grassy field, ears in the air" />
// Incorrect: Unhelpful context
<img src="beagle.png" alt="Our logo" />
```
Icons and other visual elements that aren’t strictly images should use the `aria-label` attribute. For example:
```tsx showLineNumbers {2}
<BucketTableCell>
<BucketIcon aria-label="bucket icon" size={16} />
</BucketTableCell>
```
Visual elements that are _purely_ visual aids may be removed from the accessibility tree via the `aria-hidden` attribute. For example:
```tsx showLineNumbers {2}
<BucketTableCell>
<ChevronRight aria-hidden={true} size={14} />
</BucketTableCell>
```
Never use `aria-hidden={true}` on focusable elements, since these are critical pieces of functionality.
### Scaffolding
Some scaffolding elements only make sense visually, in the context of surrounding visual content. For example: a table column for actions may not have a visual _Actions_ label because its purpose is obvious (by nearby contents) to a sighted person. For everyone else’s sake, this column should be titled with `sr-only` text:
```tsx showLineNumbers {2}
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
@@ -122,3 +122,13 @@ Displaying only an Icon in a button.
Supports slot behavior with `asChild` prop.
<ComponentPreview name="button-as-child" />
## Accessibility
[Keyboard focus](../accessibility#focus-management) is automatically handled:
- Enabled buttons default to `tabIndex={0}` (keyboard accessible)
- Disabled buttons default to `tabIndex={-1}` (removed from tab order)
- You can still override with an explicit `tabIndex` prop when needed
You therefore don't need to manually set `tabIndex`, as Button handles it automatically based on its `disabled` state.
+8 -6
View File
@@ -49,7 +49,7 @@ function app() {
### Adding new custom icons
To add a new custom icon to the Supabase icon library:
Follow these steps to add a new custom icon to the Supabase icon library.
1. **Create SVG file**: Add your SVG file to `packages/icons/src/raw-icons/` with a kebab-case name (e.g., `my-new-icon.svg`). Make sure it has follows these exact requirements:
@@ -60,16 +60,18 @@ To add a new custom icon to the Supabase icon library:
- Any unnecessary elements like `<clipPath>`, `<defs>`, and `<g>` wrappers have been removed
- SVG structure is as simple as possible with just `<path>` elements
Just leave attributes like `stroke-width` as they are. The conversion to camel-case (for React compatibility) is handled by the below build process.
Leave attributes like `stroke-width` as they are. The conversion to camel-case for React compatibility (e.g. `strokeWidth`) is handled by the below build process.
2. **Build the component**: Run `npm run build:icons` from inside the `packages/icons` directory
3. **Use the icon**: Import and use like any other icon:
```jsx
import { MyNewIcon } from 'icons'
;<MyNewIcon size={16} strokeWidth={1} />
```
```tsx
import { MyNewIcon } from 'icons'
;<MyNewIcon size={16} strokeWidth={1} />
```
The icon name is automatically made available in camel-case, determined by the kebab-case input SVG. For example, `my-new-icon.svg` will become available as `MyNewIcon`.
### SVG design guidelines
@@ -1,4 +1,4 @@
import { BucketAdd } from 'icons'
import { BucketPlus } from 'icons'
import { Plus } from 'lucide-react'
import { Button } from 'ui'
@@ -6,7 +6,7 @@ export default function EmptyStateInitialStatePresentational() {
return (
<aside className="border border-dashed w-full bg-surface-100 rounded-lg px-4 py-10 flex flex-col gap-y-4 items-center text-center gap-1 text-balance">
<div className="flex flex-col gap-3 items-center text-center">
<BucketAdd size={24} strokeWidth={1.5} className="text-foreground-muted" />
<BucketPlus size={24} strokeWidth={1.5} className="text-foreground-muted" />
<div className="flex flex-col gap-1">
<h3>Create a vector bucket</h3>
<p className="text-foreground-light text-sm">
@@ -800,6 +800,16 @@ export const auth: NavMenuConstant = {
},
],
},
{
name: 'OAuth 2.1 Server',
items: [
{ name: 'Overview', url: '/guides/auth/oauth-server' },
{ name: 'Getting Started', url: '/guides/auth/oauth-server/getting-started' },
{ name: 'OAuth Flows', url: '/guides/auth/oauth-server/oauth-flows' },
{ name: 'MCP Authentication', url: '/guides/auth/oauth-server/mcp-authentication' },
{ name: 'Token Security & RLS', url: '/guides/auth/oauth-server/token-security' },
],
},
{
name: 'Third-party auth',
enabled: allAuthProvidersEnabled,
@@ -1147,26 +1157,22 @@ export const database: NavMenuConstant = {
items: [
{ name: 'Overview', url: '/guides/database/replication' },
{
name: 'ETL Replication',
url: '/guides/database/replication/etl-replication-setup' as `/${string}`,
name: 'Replication',
url: '/guides/database/replication/replication-setup' as `/${string}`,
items: [
{
name: 'Setting up',
url: '/guides/database/replication/etl-replication-setup' as `/${string}`,
},
{
name: 'Destinations',
url: '/guides/database/replication/etl-destinations' as `/${string}`,
url: '/guides/database/replication/replication-setup' as `/${string}`,
},
{
name: 'Monitoring',
url: '/guides/database/replication/etl-replication-monitoring' as `/${string}`,
url: '/guides/database/replication/replication-monitoring' as `/${string}`,
},
{ name: 'FAQ', url: '/guides/database/replication/etl-replication-faq' },
{ name: 'FAQ', url: '/guides/database/replication/replication-faq' },
],
},
{
name: 'Manual Replication',
name: 'Manual replication',
url: '/guides/database/replication/manual-replication-setup' as `/${string}`,
items: [
{
@@ -2009,7 +2015,7 @@ export const storage: NavMenuConstant = {
},
{
name: 'Realtime Data-Sync',
url: '/guides/storage/analytics/etl' as `/${string}`,
url: '/guides/storage/analytics/replication' as `/${string}`,
},
{
name: 'Query with Postgres',
@@ -0,0 +1,65 @@
## Rate limits
Rate limits are applied to prevent abuse and ensure fair usage of the Management API. Rate limits are based on a per-user, per-scope model, meaning each user gets independent rate limits for each project and organization they interact with.
### Standard rate limit
| Limit | Duration | Scope |
| ------------ | -------- | ---------------------------------- |
| 120 requests | 1 minute | Per user, per project/organization |
When you exceed this rate limit, all subsequent API calls will return a `429 Too Many Requests` response for the remainder of the minute. Once the time window expires, your request quota resets and you can make requests again.
### Rate limit scope
Rate limits are applied with per-user + per-scope isolation:
- **Project scope**: Rate limits apply independently to each project. Requests to one project do not count toward the limit of another project.
- **Organization scope**: Rate limits apply independently to each organization. Requests to one organization do not count toward the limit of another organization.
This means you can make 120 requests to Project A and 120 requests to Project B within the same minute without hitting rate limits, as they are tracked separately.
### Rate limit response headers
Every API response includes rate limit information in the following headers:
- `X-RateLimit-Limit` - The maximum number of requests allowed in the current time window
- `X-RateLimit-Remaining` - The number of requests remaining before you hit the rate limit
- `X-RateLimit-Reset` - The number of milliseconds remaining until your rate limit resets
You can use these headers to monitor your usage and implement proactive rate limit handling before receiving a 429 response.
### How rate limits are tracked
Your requests are identified and tracked using one of the following identifiers, in this order of priority:
1. **OAuth App ID** - If your request is authenticated via an OAuth application
2. **User ID** - If your request is authenticated with a personal access token
3. **IP Address** - If your request is unauthenticated (extracted from request headers)
Each identifier is combined with the scope (project or organization) to create a unique tracking key. This ensures that rate limits are isolated per user and per scope, preventing one project or organization from affecting another.
### Endpoint exceptions
Some endpoints have stricter rate limits than the standard 120 requests per minute to prevent abuse of resource-intensive operations:
| Endpoint | Limit | Duration | Reason |
| ---------------------------------------------------------- | ----------- | -------- | --------------------------------------------------- |
| `GET /v1/projects/:ref/endpoints/logs.all` | 30 requests | 1 minute | Analytics log queries are computationally expensive |
| `GET /v1/projects/:ref/endpoints/usage.api-counts` | 30 requests | 1 minute | Analytics aggregation is computationally expensive |
| `GET /v1/projects/:ref/endpoints/usage.api-requests-count` | 30 requests | 1 minute | Analytics aggregation is computationally expensive |
| `GET /v1/projects/:ref/database/context` | 10 requests | 1 minute | Database context operations are resource-intensive |
| `GET /v1/projects/:ref/database/context` | 1 request | 1 second | Burst limit to prevent rapid successive requests |
| `POST /v1/projects/:ref/config/custom-hostname/initialize` | 10 requests | 1 minute | These operations are expensive |
| `POST /v1/projects/:ref/config/custom-hostname/reverify` | 10 requests | 1 minute | These operations are expensive |
| `DELETE /v1/projects/:ref/config/custom-hostname` | 10 requests | 1 minute | These operations are expensive |
| `GET /v1/projects/:ref/config/vanity-subdomain` | 10 requests | 1 minute | These operations are expensive |
**Note:** The `GET /v1/projects/:ref/database/context` endpoint has dual rate limiting. You can make up to 10 requests per minute, but also no more than 1 request per second to prevent burst traffic.
### Best practices
- **Monitor rate limit headers** - Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit.
- **Implement exponential backoff** - When you receive a 429 response, wait before retrying. You can use the `X-RateLimit-Reset` header (milliseconds) to determine exactly how long to wait.
- **Batch operations** - Where possible, combine multiple operations into fewer API calls to reduce your request count.
- **Be mindful of expensive endpoints** - Analytics, database context, and domain endpoints have stricter limits, so use them judiciously.
@@ -16,11 +16,11 @@ Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`,
**Inputs**
| Field | Type | Description |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_id` | `string` | Unique identifier for the user attempting to sign in. |
| `claims` | `object` | Claims which are included in the access token. |
| `authentication_method` | `string` | The authentication method used to request the access token. Possible values include: `oauth`, `password`, `otp`, `totp`, `recovery`, `invite`, `sso/saml`, `magiclink`, `email/signup`, `email_change`, `token_refresh`, `anonymous`. |
| Field | Type | Description |
| ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_id` | `string` | Unique identifier for the user attempting to sign in. |
| `claims` | `object` | Claims which are included in the access token. |
| `authentication_method` | `string` | The authentication method used to request the access token. Possible values include: `oauth`, `password`, `otp`, `totp`, `recovery`, `invite`, `sso/saml`, `magiclink`, `email/signup`, `email_change`, `token_refresh`, `oauth_provider/authorization_code`, `anonymous`. |
<Tabs
scrollable
@@ -45,7 +45,8 @@ Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`,
"aal": "aal1",
"amr": [ { "method": "anonymous", "timestamp": 1715686621 } ],
"session_id": "4b938a09-5372-4177-a314-cfa292099ea2",
"is_anonymous": true
"is_anonymous": true,
"client_id": "oauth-client-id-if-oauth-flow"
},
"authentication_method": "anonymous"
}
@@ -143,6 +144,10 @@ Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`,
"is_anonymous": {
"type": "boolean",
"x-faker": "random.boolean"
},
"client_id": {
"type": "string",
"x-faker": "random.uuid"
}
},
"required": [
@@ -175,6 +180,7 @@ Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`,
"email/signup",
"email_change",
"token_refresh",
"oauth_provider/authorization_code",
"anonymous"
]
}
@@ -0,0 +1,91 @@
---
title: 'OAuth 2.1 Server'
description: 'Turn your Supabase project into an OAuth 2.1 and OpenID Connect identity provider'
---
Supabase Auth can act as an OAuth 2.1 and OpenID Connect (OIDC) identity provider. This allows other applications and services to use your Supabase project as their authentication provider, just like "Sign in with Google" or "Sign in with GitHub".
You can use this to build "Sign in with [Your App]" experiences, authenticate AI agents through the Model Context Protocol (MCP), power developer platforms with third-party integrations, or implement standards-compliant enterprise SSO.
## Use cases
There are several reasons why you might want to enable OAuth 2.1 Server in your Supabase project:
- **Developer platforms and marketplaces**: Allow third-party developers to build integrations and apps for your platform. Partners can offer "Sign in with [Your App]" to their users, with your control over data access through Row Level Security policies.
- **AI agents and automation**: Authenticate AI agents, LLM tools, and MCP servers that need to access user data. The Model Context Protocol provides automatic OAuth discovery and client registration for AI applications.
- **Mobile and desktop apps**: Issue OAuth tokens to your own mobile apps, desktop applications, or other first-party clients. All tokens respect your existing Row Level Security policies and work with Custom Access Token Hooks.
- **Enterprise SSO**: Provide OpenID Connect (OIDC) authentication for enterprise customers who need standards-compliant identity federation across multiple services.
## Overview
Supabase Auth implements the OAuth 2.1 authorization code flow with PKCE (Proof Key for Code Exchange). When a third-party application wants to access user data:
1. The application redirects the user to your authorization endpoint
2. Supabase Auth validates the request and redirects to your custom authorization UI
3. The user authenticates (using any of your enabled auth methods) and approves access
4. Supabase Auth issues an authorization code
5. The application exchanges the code for access and refresh tokens
6. The application uses the access token to make authenticated API requests
Access tokens are standard Supabase JWTs that include `user_id`, `role`, and `client_id` claims. Your existing Row Level Security policies automatically apply to OAuth tokens, giving you fine-grained control over what each client can access.
### Supported standards
- **OAuth 2.1**: Latest OAuth specification with mandatory PKCE
- **OpenID Connect**: ID tokens (with `openid` scope), UserInfo endpoint, and OIDC discovery
- **Standard scopes**: `openid`, `email`, `profile`, and `phone` scopes for controlling data access
- **Dynamic client registration**: Automatic registration for MCP-compatible clients
- **JWKS endpoint**: Public keys for third parties to validate tokens
### Integration with existing auth
OAuth 2.1 Server works seamlessly with your existing Supabase Auth configuration:
- Users can authenticate using any enabled method (password, magic link, social providers, MFA, phone)
- [Custom Access Token Hooks](/guides/auth/auth-hooks/access-token-hook) apply to OAuth tokens, allowing you to customize claims like `audience` or add client-specific permissions
- Row Level Security policies control data access using the `client_id` claim in tokens
- All standard Supabase features (email templates, hooks, rate limiting) continue to work
## Set up OAuth 2.1 server
To enable OAuth 2.1 Server in your project, follow these guides:
<div className="grid md:grid-cols-12 gap-4 not-prose">
{[
{
name: 'Getting Started',
description:
'Enable OAuth 2.1, configure your authorization endpoint, and register your first client.',
href: '/guides/auth/oauth-server/getting-started',
},
{
name: 'OAuth Flows',
description: 'Detailed walkthrough of authorization code and refresh token flows.',
href: '/guides/auth/oauth-server/oauth-flows',
},
{
name: 'MCP Authentication',
description: 'Authenticate AI agents and LLM tools using Model Context Protocol.',
href: '/guides/auth/oauth-server/mcp-authentication',
},
{
name: 'Token Security & RLS',
description: 'Control data access with Row Level Security policies for OAuth clients.',
href: '/guides/auth/oauth-server/token-security',
},
].map((x) => (
<div className="col-span-6" key={x.href}>
<Link href={x.href} passHref>
<GlassPanel title={x.name}>{x.description}</GlassPanel>
</Link>
</div>
))}
</div>
## Resources
- [GitHub Discussion](https://github.com/orgs/supabase/discussions/38022) - Share your use cases and help shape the roadmap
- [Discord Community](https://discord.supabase.com/) - Get help and share what you're building
@@ -0,0 +1,614 @@
---
id: 'oauth-server-getting-started'
title: 'Getting Started with OAuth 2.1 Server'
description: 'Learn how to enable OAuth 2.1 and register client applications in your Supabase project'
---
This guide will walk you through setting up your Supabase project as an OAuth 2.1 identity provider, from enabling the feature to registering your first client application.
## Prerequisites
Before you begin, make sure you have:
- A Supabase project (create one at [supabase.com](https://supabase.com))
- Admin access to your project
- (Optional) [Supabase CLI](/docs/guides/cli) v2.54.11 or higher for local development
## Overview
Setting up OAuth 2.1 in your Supabase project involves these steps:
1. Enable OAuth 2.1 server capabilities in your project
2. Configure your authorization path
3. Build your authorization UI (frontend)
4. Register OAuth client applications
<Admonition type="note">
Testing OAuth flows is often easier on a Supabase project since it's already accessible on the web, no tunnel or additional configuration needed.
</Admonition>
## Enable OAuth 2.1 server
OAuth 2.1 server is currently in beta and free to use during the beta period on all Supabase plans.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="oauth-setup"
>
<TabPanel id="dashboard" label="Cloud">
1. Go to your project dashboard
2. Navigate to **Authentication** > **OAuth Server** in the sidebar
3. Enable OAuth 2.1 server capabilities
</TabPanel>
<TabPanel id="cli" label="CLI">
Edit your `supabase/config.toml` file and add the OAuth server configuration:
```toml
[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent"
allow_dynamic_registration = false # Optional: enable dynamic client registration
```
Start or restart your local Supabase instance:
```bash
supabase start
# or if already running:
supabase stop && supabase start
```
</TabPanel>
</Tabs>
Once enabled, your project will expose the necessary OAuth endpoints:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="oauth-setup"
>
<TabPanel id="dashboard" label="Cloud">
| Endpoint | URL |
| -------------------------- | ---------------------------------------------------------------------------------- |
| **Authorization endpoint** | `https://<project-ref>.supabase.co/auth/v1/oauth/authorize` |
| **Token endpoint** | `https://<project-ref>.supabase.co/auth/v1/oauth/token` |
| **JWKS endpoint** | `https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json` |
| **Discovery endpoint** | `https://<project-ref>.supabase.co/.well-known/oauth-authorization-server/auth/v1` |
| **OIDC discovery** | `https://<project-ref>.supabase.co/auth/v1/.well-known/openid-configuration` |
</TabPanel>
<TabPanel id="cli" label="CLI">
| Endpoint | URL |
| -------------------------- | ----------------------------------------------------------------------- |
| **Authorization endpoint** | `http://localhost:54321/auth/v1/oauth/authorize` |
| **Token endpoint** | `http://localhost:54321/auth/v1/oauth/token` |
| **JWKS endpoint** | `http://localhost:54321/auth/v1/.well-known/jwks.json` |
| **Discovery endpoint** | `http://localhost:54321/.well-known/oauth-authorization-server/auth/v1` |
| **OIDC discovery** | `http://localhost:54321/auth/v1/.well-known/openid-configuration` |
### Expose local instance to the world
To test OAuth flows with external applications, you can expose your local Supabase instance using a tunnel solution (such as [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/)).
When using a tunnel, configure the `jwt_issuer` field in your `supabase/config.toml` to match your tunnel URL:
```toml
[auth]
jwt_issuer = "https://my-tunnel.url/auth/v1"
```
This ensures that JWTs issued by your local instance use the correct issuer claim for token validation, and serves the discovery endpoint at the correct location with accurate discovery information.
</TabPanel>
</Tabs>
<Admonition type="tip">
**Use asymmetric JWT signing keys for better security**
By default, Supabase uses HS256 (symmetric) for signing JWTs. For OAuth use cases, we recommend migrating to asymmetric algorithms like RS256 or ES256. Asymmetric keys are more scalable and secure because:
- OAuth clients can validate JWTs using the public key from your JWKS endpoint
- No need to share your JWT secret with third-party applications
- More resilient architecture for distributed systems
Learn more about [configuring JWT signing keys](/docs/guides/auth/signing-keys).
**Note:** If you plan to use OpenID Connect ID tokens (by requesting the `openid` scope), asymmetric signing algorithms are **required**. ID token generation will fail with HS256.
</Admonition>
## Configure your authorization path
Before registering clients, you need to configure where your authorization UI will live.
1. In your project dashboard, navigate to **Authentication** > **OAuth Server**
2. Set the **Authorization Path** (e.g., `/oauth/consent`)
<Admonition type="note">
The authorization path is combined with your Site URL (configured in **Authentication** > **URL Configuration**) to create the full authorization endpoint URL.
</Admonition>
Your authorization UI will be at the combined Site URL + Authorization Path. For example:
- Site URL: `https://example.com` (from **Authentication** > **URL Configuration**)
- Authorization Path: `/oauth/consent` (from **OAuth Server** settings)
- Your authorization UI: `https://example.com/oauth/consent`
When OAuth clients initiate the authorization flow, Supabase Auth will redirect users to this URL with an `authorization_id` query parameter. You'll use [Supabase JavaScript library OAuth methods](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/GoTrueClient.ts#L2159-L2163) to handle the authorization:
- `supabase.auth.oauth.getAuthorizationDetails(authorization_id)` - Retrieve client and authorization details
- `supabase.auth.oauth.approveAuthorization(authorization_id)` - Approve the authorization request
- `supabase.auth.oauth.denyAuthorization(authorization_id)` - Deny the authorization request
## Build your authorization UI
This is where you build the **frontend** for your authorization flow. When third-party apps initiate OAuth, users will be redirected to your authorization path (configured in the previous step) with an `authorization_id` query parameter.
Your authorization UI should:
1. **Extract authorization_id** - Get the `authorization_id` from the URL query parameters
2. **Authenticate the user** - If not already logged in, redirect to your login page (preserving the authorization_id)
3. **Retrieve authorization details** - Use `supabase.auth.oauth.getAuthorizationDetails(authorization_id)` to get client information including requested scopes
4. **Display consent screen** - Show the user what app is requesting access and what scopes/permissions are being requested
5. **Handle user decision** - Call either `approveAuthorization(authorization_id)` or `denyAuthorization(authorization_id)` based on user choice
The authorization details include a `scopes` field that contains the scopes requested by the client (e.g., `["openid", "email", "profile"]`). You should display these scopes to the user so they understand what information will be shared.
<Admonition type="note">
This is a **frontend implementation**. You're building the UI that displays the consent screen and handles user interactions. The actual OAuth token generation is handled by Supabase Auth after you call the approve/deny methods.
</Admonition>
### Example authorization UI
Here's how to build a minimal authorization page at your configured path (e.g., `/oauth/consent`):
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="nextjs"
queryGroup="framework"
>
<TabPanel id="nextjs" label="Next.js">
```tsx
// app/oauth/consent/page.tsx
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function ConsentPage({
searchParams,
}: {
searchParams: { authorization_id?: string }
}) {
const authorizationId = searchParams.authorization_id
if (!authorizationId) {
return <div>Error: Missing authorization_id</div>
}
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: async () => (await cookies()).getAll(),
setAll: async (cookiesToSet) => {
const cookieStore = await cookies()
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
},
},
}
)
// Check if user is authenticated
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
// Redirect to login, preserving authorization_id
redirect(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`)
}
// Get authorization details using the authorization_id
const { data: authDetails, error } =
await supabase.auth.oauth.getAuthorizationDetails(authorizationId)
if (error || !authDetails) {
return <div>Error: {error?.message || 'Invalid authorization request'}</div>
}
return (
<div>
<h1>Authorize {authDetails.client.name}</h1>
<p>This application wants to access your account.</p>
<div>
<p>
<strong>Client:</strong> {authDetails.client.name}
</p>
<p>
<strong>Redirect URI:</strong> {authDetails.redirect_uri}
</p>
{authDetails.scopes && authDetails.scopes.length > 0 && (
<div>
<strong>Requested permissions:</strong>
<ul>
{authDetails.scopes.map((scope) => (
<li key={scope}>{scope}</li>
))}
</ul>
</div>
)}
</div>
<form action="/api/oauth/decision" method="POST">
<input type="hidden" name="authorization_id" value={authorizationId} />
<button type="submit" name="decision" value="approve">
Approve
</button>
<button type="submit" name="decision" value="deny">
Deny
</button>
</form>
</div>
)
}
```
```typescript
// app/api/oauth/decision/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const formData = await request.formData()
const decision = formData.get('decision')
const authorizationId = formData.get('authorization_id') as string
if (!authorizationId) {
return NextResponse.json({ error: 'Missing authorization_id' }, { status: 400 })
}
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: async () => (await cookies()).getAll(),
setAll: async (cookiesToSet) => {
const cookieStore = await cookies()
cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
},
},
}
)
if (decision === 'approve') {
const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 })
}
// Redirect back to the client with authorization code
return NextResponse.redirect(data.redirect_to)
} else {
const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 })
}
// Redirect back to the client with error
return NextResponse.redirect(data.redirect_to)
}
}
```
</TabPanel>
<TabPanel id="react" label="React (SPA)">
```tsx
// src/pages/OAuthConsent.tsx
import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { supabase } from './supabaseClient'
export function OAuthConsent() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const authorizationId = searchParams.get('authorization_id')
const [authDetails, setAuthDetails] = useState<any>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function loadAuthDetails() {
if (!authorizationId) {
setError('Missing authorization_id')
setLoading(false)
return
}
// Check if user is authenticated
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
navigate(`/login?redirect=/oauth/consent?authorization_id=${authorizationId}`)
return
}
// Get authorization details using the authorization_id
const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)
if (error) {
setError(error.message)
} else {
setAuthDetails(data)
}
setLoading(false)
}
loadAuthDetails()
}, [authorizationId, navigate])
async function handleApprove() {
if (!authorizationId) return
const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId)
if (error) {
setError(error.message)
} else {
// Redirect to client app
window.location.href = data.redirect_to
}
}
async function handleDeny() {
if (!authorizationId) return
const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId)
if (error) {
setError(error.message)
} else {
// Redirect to client app with error
window.location.href = data.redirect_to
}
}
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!authDetails) return <div>No authorization request found</div>
return (
<div>
<h1>Authorize {authDetails.client.name}</h1>
<p>This application wants to access your account.</p>
<div>
<p>
<strong>Client:</strong> {authDetails.client.name}
</p>
<p>
<strong>Redirect URI:</strong> {authDetails.redirect_uri}
</p>
{authDetails.scopes && authDetails.scopes.length > 0 && (
<div>
<strong>Requested permissions:</strong>
<ul>
{authDetails.scopes.map((scope) => (
<li key={scope}>{scope}</li>
))}
</ul>
</div>
)}
</div>
<div>
<button onClick={handleApprove}>Approve</button>
<button onClick={handleDeny}>Deny</button>
</div>
</div>
)
}
```
</TabPanel>
</Tabs>
### How it works
1. **User navigates to your authorization path** - When a third-party app initiates OAuth, Supabase Auth redirects the user to your configured authorization path (e.g., `https://example.com/oauth/consent?authorization_id=<id>`)
2. **Extract authorization_id** - Your page extracts the `authorization_id` from the URL query parameters
3. **Check authentication** - Your page checks if the user is logged in, redirecting to login if not (preserving the authorization_id)
4. **Retrieve details** - Call `supabase.auth.oauth.getAuthorizationDetails(authorization_id)` to get information about the requesting client
5. **Show consent screen** - Display a UI asking the user to approve or deny access
6. **Handle decision** - When the user clicks approve/deny:
- Call `supabase.auth.oauth.approveAuthorization(authorization_id)` or `denyAuthorization(authorization_id)`
- These methods handle all OAuth logic internally (generating authorization codes, etc.)
- They return a `redirect_to` URL
7. **Redirect back** - Redirect the user to the `redirect_to` URL, which sends them back to the third-party app with either an authorization code (approved) or error (denied)
## Register an OAuth client
Before third-party applications can use your project as an identity provider, you need to register them as OAuth clients.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="dashboard"
queryGroup="oauth-setup"
>
<TabPanel id="dashboard" label="Dashboard">
1. Go to **Authentication** > **OAuth Apps** (under the **Manage** section)
2. Click **Add a new client**
3. Enter the client details:
- **Client name**: A friendly name for your application
- **Redirect URIs**: One or more URLs where users will be redirected after authorization
- **Client type**: Choose between:
- **Public** - For mobile and single-page apps (no client secret)
- **Confidential** - For server-side apps (includes client secret)
4. Click **Create**
You'll receive:
- **Client ID**: A unique identifier for the client
- **Client Secret** (for confidential clients): A secret key for authenticating the client
<Admonition type="caution">
Store the client secret securely. It will only be shown once. If you lose it, you can regenerate a new one from the **OAuth Apps** page.
</Admonition>
</TabPanel>
<TabPanel id="programmatically" label="Programmatically">
You can register clients programmatically by calling your project's auth server admin endpoint with a secret key:
**Production:**
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/admin/oauth/clients' \
-H "Authorization: Bearer ${SUPABASE_SECRET_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "My Third-Party App",
"redirect_uris": [
"https://my-app.com/auth/callback",
"https://my-app.com/auth/silent-callback"
],
"client_type": "confidential"
}'
```
**Local development:**
```bash
curl -X POST 'http://localhost:54321/auth/v1/admin/oauth/clients' \
-H "Authorization: Bearer ${SUPABASE_SECRET_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "Local Dev App",
"redirect_uris": ["http://localhost:3000/auth/callback"],
"client_type": "confidential"
}'
```
Response:
```json
{
"client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"client_secret": "verysecret-1234567890abcdef...",
"name": "My Third-Party App",
"redirect_uris": ["https://my-app.com/auth/callback", "https://my-app.com/auth/silent-callback"],
"client_type": "confidential",
"created_at": "2025-01-15T10:30:00.000Z"
}
```
<Admonition type="note">
For complete API documentation, see the [OAuth Admin API reference](/todo).
</Admonition>
### List OAuth clients
To view all registered OAuth clients:
**Production:**
```bash
curl 'https://<project-ref>.supabase.co/auth/v1/admin/oauth/clients' \
-H "Authorization: Bearer ${SUPABASE_SECRET_KEY}"
```
**Local development:**
```bash
curl 'http://localhost:54321/auth/v1/admin/oauth/clients' \
-H "Authorization: Bearer ${SUPABASE_SECRET_KEY}"
```
</TabPanel>
</Tabs>
## Customizing tokens (optional)
By default, OAuth access tokens include standard claims like `user_id`, `role`, and `client_id`. If you need to customize tokens—for example, to set a specific `audience` claim for third-party validation or add client-specific metadata—use [Custom Access Token Hooks](/docs/guides/auth/auth-hooks/access-token-hook).
Custom Access Token Hooks are triggered for all token issuance, including OAuth flows. You can use the `client_id` parameter to customize tokens based on which OAuth client is requesting them.
### Common use cases
- **Customize `audience` claim**: Set the `aud` claim to the third-party API endpoint for proper JWT validation
- **Add client-specific permissions**: Include custom claims based on which OAuth client is requesting access
- **Implement dynamic scopes**: Add metadata that RLS policies can use for fine-grained access control
For more examples, see [Token Security & RLS](/docs/guides/auth/oauth-server/token-security#custom-access-token-hooks).
## Redirect URI configuration
Redirect URIs are critical for OAuth security. Supabase Auth will only redirect to URIs that are explicitly registered with the client.
<Admonition type="note">
**Not to be confused with general redirect URLs**
This section is about **OAuth client redirect URIs** - where to send users after they authorize third-party apps to access your Supabase project. This is different from the general [Redirect URLs](/docs/guides/auth/redirect-urls) setting, which controls where to send users after they sign in TO your app using social providers.
</Admonition>
<Admonition type="caution">
**Exact matches only - No wildcards or patterns**
OAuth client redirect URIs require exact, complete URL matches. Unlike general redirect URLs (which support wildcards), OAuth client redirect URIs do NOT support wildcards, patterns, or partial URLs. You must register the full, exact callback URL.
</Admonition>
### Best practices
- **Use HTTPS in production** - Always use HTTPS for redirect URIs in production
- **Register exact, complete URLs** - Each redirect URI must be the full URL including protocol, domain, path, and port if needed
- **Use separate OAuth clients per environment** - Create separate OAuth clients for development, staging, and production. This provides better security isolation, allows independent secret rotation, and improves auditability. If you need to use the same client across environments, you can register multiple redirect URIs, but separate clients are recommended.
## Next steps
Now that you've registered your first OAuth client, you're ready to:
- [Understand OAuth flows](/docs/guides/auth/oauth-server/oauth-flows) - Learn how the authorization code and refresh token flows work
- [Implement MCP authentication](/docs/guides/auth/oauth-server/mcp-authentication) - Enable AI agent authentication
- [Secure with RLS](/docs/guides/auth/oauth-server/token-security) - Control data access for OAuth clients
@@ -0,0 +1,156 @@
---
id: 'oauth-server-mcp'
title: 'Model Context Protocol (MCP) Authentication'
description: 'Integrate Supabase Auth with MCP servers to authenticate AI agents using your existing user base'
---
The Model Context Protocol (MCP) is an open standard for connecting AI agents and LLM tools to data sources and services. While Supabase doesn't provide MCP server functionality, you can build your own MCP servers that connect to your Supabase project and leverage Supabase Auth's OAuth 2.1 capabilities to authenticate AI agents using your existing user base.
## Why use Supabase Auth for MCP?
When building MCP servers that connect to your Supabase project, you can leverage your existing Supabase Auth infrastructure to authenticate AI agents:
- **Use your existing user base** - No need to create separate authentication systems; AI agents authenticate as your existing users
- **Standards-compliant OAuth 2.1** - Full implementation with PKCE that MCP clients expect
- **Automatic discovery** - MCP clients auto-configure using Supabase's discovery endpoints
- **Dynamic client registration** - MCP clients can register themselves automatically with your project
- **Row Level Security** - Your existing RLS policies automatically apply to MCP clients
- **User authorization** - Users explicitly approve AI agent access through your authorization flow
- **Token management** - Automatic refresh token rotation and expiration handled by Supabase
## How MCP authentication works
When you build an MCP server that connects to your Supabase project, authentication flows through Supabase Auth:
1. **Discovery**: The MCP client fetches your OAuth configuration from Supabase's discovery endpoint
2. **Registration** (optional): The client registers itself as an OAuth client in your Supabase project
3. **Authorization**: User is redirected to your authorization endpoint to approve the AI tool's access
4. **Token exchange**: Supabase issues access and refresh tokens for the authenticated user
5. **Authenticated access**: The MCP server can now make requests to your Supabase APIs on behalf of the user
By leveraging Supabase Auth, your MCP server can authenticate AI agents using your existing user accounts without building a separate authentication system.
## Prerequisites
Before setting up MCP authentication:
- [Enable OAuth 2.1 server](/docs/guides/auth/oauth-server/getting-started) in your Supabase project
- Build an [authorization endpoint](/docs/guides/auth/oauth-server/getting-started#build-your-authorization-endpoint)
- (Optional) Enable dynamic client registration
## Setting up your MCP server
Configure your MCP server to use your Supabase Auth server:
```
https://<project-ref>.supabase.co/auth/v1
```
Replace `<project-ref>` with your project reference ID from the Supabase dashboard.
MCP clients will automatically discover your OAuth configuration from:
```
https://<project-ref>.supabase.co/.well-known/oauth-authorization-server/auth/v1
```
### OAuth client setup
Depending on your MCP server implementation, you have two options:
- **Pre-register an OAuth client** - Manually register your client by following the [Register an OAuth client](/docs/guides/auth/oauth-server/getting-started#register-an-oauth-client) guide and use the client credentials in your MCP server
- **Dynamic client registration** - Enable this in **Authentication** > **OAuth Server** in your Supabase dashboard to allow MCP clients to register themselves automatically without manual intervention
<Admonition type="caution">
Dynamic registration allows any MCP client to register with your project. Consider:
- Requiring user approval for all clients
- Monitoring registered clients regularly
- Validating redirect URIs are from trusted domains
</Admonition>
## Building an MCP server with Supabase Auth
When building your own MCP server, integrate with Supabase Auth to authenticate AI agents as your existing users and leverage your RLS policies.
<Admonition type="tip">
**Looking for an easier way to build MCP servers?**
[FastMCP](https://gofastmcp.com) provides a streamlined way to build MCP servers with built-in Supabase Auth integration. FastMCP handles OAuth configuration, token management, and authentication flows automatically, letting you focus on building your AI agent's functionality. Check out their [Supabase integration guide](https://gofastmcp.com/todo) to get started quickly.
</Admonition>
## Handling MCP tokens in your application
When your MCP server makes requests to your Supabase APIs on behalf of authenticated users, it will send access tokens issued by Supabase Auth, just like any other OAuth client.
### Validating MCP tokens
Use the same token validation as other OAuth clients.
See [Token Security & RLS](/docs/guides/auth/oauth-server/token-security) for more examples.
## Security considerations
### User approval
Always require explicit user approval for MCP clients:
- Show clear information about what the AI agent can access
- Display the client name and description
- List the scopes being requested
- Provide an option to deny access
- Allow users to revoke access later
## Troubleshooting
### MCP client can't discover OAuth configuration
**Problem**: Client shows "OAuth discovery failed" or similar error.
**Solutions**:
- Verify OAuth 2.1 is enabled in your project
- Check that `/.well-known/oauth-authorization-server` returns valid JSON
- Ensure your project URL is accessible
### Dynamic registration fails
**Problem**: Client receives 403 or 404 on registration endpoint.
**Solutions**:
- Enable dynamic client registration in project settings
- Verify redirect URIs are valid, complete URLs (protocol, domain, path, and port)
- Check for rate limiting on registration endpoint
### Token exchange fails
**Problem**: Client receives "invalid_grant" error.
**Solutions**:
- Verify authorization code hasn't expired (10 minutes)
- Ensure code verifier matches code challenge
- Check that redirect URI exactly matches registration
- Confirm client_id is correct
### RLS policies block MCP access
**Problem**: MCP client can't access data despite valid token.
**Solutions**:
- Check RLS policies include the MCP client's `client_id`
- Verify user has necessary permissions
- Test with service role key to isolate RLS issues
- Review [Token Security guide](/docs/guides/auth/oauth-server/token-security)
## Next steps
- [Secure with RLS](/docs/guides/auth/oauth-server/token-security) - Create granular policies for MCP clients
- [OAuth flows](/docs/guides/auth/oauth-server/oauth-flows) - Deep dive into OAuth implementation
- [MCP Specification](https://modelcontextprotocol.io/docs) - Official MCP documentation
@@ -0,0 +1,767 @@
---
id: 'oauth-server-flows'
title: 'OAuth 2.1 Flows'
description: 'Understanding the authorization code and refresh token flows with OpenID Connect'
---
Supabase Auth implements OAuth 2.1 with OpenID Connect (OIDC), supporting the authorization code flow with PKCE and refresh token flow. This guide explains how these flows work in detail.
<Admonition type="note">
This guide explains the OAuth 2.1 flows for **third-party client applications** that authenticate with your Supabase project. These flows require custom implementation and are not available in the `@supabase/supabase-js` library. The `supabase-js` library is for authenticating **with** Supabase Auth as an identity provider, not for building your own OAuth server.
</Admonition>
## Supported grant types
Supabase Auth supports two OAuth 2.1 grant types:
1. **Authorization Code with PKCE** (`authorization_code`) - For obtaining initial access tokens
2. **Refresh Token** (`refresh_token`) - For obtaining new access tokens without re-authentication
<Admonition type="note">
Other grant types like `client_credentials` or `password` are not supported.
</Admonition>
## Authorization code flow with PKCE
The authorization code flow with PKCE (Proof Key for Code Exchange) is the recommended flow for all OAuth clients, including single-page applications, mobile apps, and server-side applications.
### How it works
The flow consists of several steps:
1. **Client initiates authorization** - Third-party app redirects user to Supabase Auth's authorize endpoint
2. **Supabase validates and redirects** - Supabase Auth validates OAuth parameters and redirects user to your configured authorization URL
3. **User authenticates and authorizes** - Your frontend checks if user is logged in, shows consent screen, and handles approval/denial
4. **Authorization code issued** - Supabase Auth generates a short-lived authorization code and redirects back to client
5. **Code exchange** - Client exchanges the code for tokens
6. **Access granted** - Client receives access token, refresh token, and ID token
### Flow diagram
Here's a visual representation of the complete authorization code flow:
```
┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ │ │ │ │ │
│ Client │ │ Your Auth UI │ │ Supabase Auth │
│ App │ │ (Frontend) │ │ │
│ │ │ │ │ │
└──────┬──────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
│ 1. Generate PKCE params │
│ (code_verifier, code_challenge) │
│ │ │
│ 2. Redirect to /oauth/authorize with code_challenge │
├────────────────────────────────────────────────────────────────>│
│ │ │
│ │ 3. Validate params & redirect │
│ │ to authorization_path │
│ │<────────────────────────────────┤
│ │ │
│ │ 4. getAuthorizationDetails() │
│ ├────────────────────────────────>│
│ │ Return client info │
│ │<────────────────────────────────┤
│ │ │
│ │ 5. User login & consent │
│ │ │
│ │ 6. approveAuthorization() │
│ ├────────────────────────────────>│
│ │ Return redirect_to with code │
│ │<────────────────────────────────┤
│ │ │
│ 7. Redirect to client callback with code │
│<───────────────────────────────────────────────────────────────┤
│ │ │
│ 8. Exchange code for tokens (POST /oauth/token) │
│ with code_verifier │
├────────────────────────────────────────────────────────────────>│
│ │ │
│ 9. Return tokens (access, refresh, ID) │
│<────────────────────────────────────────────────────────────────┤
│ │ │
│ 10. Access resources with access_token │
│ │ │
│ 11. Refresh tokens (POST /oauth/token with refresh_token) │
├────────────────────────────────────────────────────────────────>│
│ │ │
│ 12. Return new tokens │
│<────────────────────────────────────────────────────────────────┤
│ │ │
```
**Key points:**
- Third-party client redirects user to **Supabase Auth's authorize endpoint** (not directly to your UI)
- Supabase Auth validates OAuth parameters and redirects to **your authorization path**
- Your frontend UI handles authentication and consent using `supabase-js` OAuth methods
- Supabase Auth handles all backend OAuth logic (code generation, token issuance)
### Step 1: Generate PKCE parameters
Before initiating the flow, the client must generate PKCE parameters:
```javascript
// Generate a random code verifier (43-128 characters)
function generateCodeVerifier() {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return base64URLEncode(array)
}
// Create code challenge from verifier
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder()
const data = encoder.encode(verifier)
const hash = await crypto.subtle.digest('SHA-256', data)
return base64URLEncode(new Uint8Array(hash))
}
function base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
// Generate and store verifier (you'll need it later)
const codeVerifier = generateCodeVerifier()
sessionStorage.setItem('code_verifier', codeVerifier)
// Generate challenge to send in authorization request
const codeChallenge = await generateCodeChallenge(codeVerifier)
```
### Step 2: Authorization request
The client redirects the user to your authorization endpoint with the following parameters:
```
https://<project-ref>.supabase.co/auth/v1/oauth/authorize?
response_type=code
&client_id=<client-id>
&redirect_uri=<configured-redirect-uri>
&state=<random-state>
&code_challenge=<code-challenge>
&code_challenge_method=S256
```
#### Required parameters
| Parameter | Description |
| ----------------------- | -------------------------------------------- |
| `response_type` | Must be `code` for authorization code flow |
| `client_id` | The client ID from registration |
| `redirect_uri` | Must exactly match a registered redirect URI |
| `code_challenge` | The generated code challenge |
| `code_challenge_method` | Must be `S256` (SHA-256) |
#### Optional parameters
| Parameter | Description |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | Random string to prevent CSRF attacks (highly recommended) |
| `scope` | Space-separated list of scopes (e.g., `openid email profile phone`). Requested scopes will be included in the access token and control what information is returned by the UserInfo endpoint. Default scope when none provided is `email`. If the `openid` scope is requested, an ID token will be included in the response. |
| `nonce` | Random string for replay attack protection. If provided, will be included in the ID token. |
<Admonition type="caution">
Always include a `state` parameter to protect against CSRF attacks. Generate a random string, store it in session storage, and verify it matches when the user returns.
</Admonition>
### Step 3: User authentication and consent
After receiving the authorization request, Supabase Auth validates the OAuth parameters (client_id, redirect_uri, PKCE, etc.) and then redirects the user to your configured **authorization path** (e.g., `https://example.com/oauth/consent?authorization_id=<id>`).
The URL will contain an `authorization_id` query parameter that identifies this authorization request.
Your frontend application at the authorization path should:
1. **Extract authorization_id** - Get the `authorization_id` from the URL query parameters
2. **Fetch authorization details** - Call `supabase.auth.oauth.getAuthorizationDetails(authorization_id)` to retrieve information about the OAuth client and request parameters
3. **Check user authentication** - Verify if the user is logged in; if not, redirect to your login page (preserving the full authorization path including the `authorization_id`). After successful login, redirect the user back to the authorization path with the same `authorization_id` query parameter
4. **Display consent screen** - Show the user information about the requesting client (name, redirect URI, scopes)
5. **Handle user decision** - When the user approves or denies:
- Call `supabase.auth.oauth.approveAuthorization(authorization_id)` to approve
- Call `supabase.auth.oauth.denyAuthorization(authorization_id)` to deny
- Redirect user to the returned `redirect_to` URL
This is a **frontend implementation** using `supabase-js`. Supabase Auth handles all the backend OAuth logic (generating authorization codes, validating requests, etc.) after you call the approve/deny methods.
See the [Getting Started guide](/docs/guides/auth/oauth-server/getting-started#example-authorization-ui) for complete implementation examples.
### Step 4: Authorization code issued
If the user approves access, Supabase Auth redirects back to the client's redirect URI with an authorization code:
```
https://client-app.com/callback?
code=<authorization-code>
&state=<state-from-request>
```
The authorization code is:
- **Short-lived** - Valid for 10 minutes
- **Single-use** - Can only be exchanged once
- **Bound to PKCE** - Can only be exchanged with the correct code verifier
If the user denies access, Supabase Auth redirects with error information in query parameters:
```
https://client-app.com/callback?
error=access_denied
&error_description=The+user+denied+the+authorization+request
&state=<state-from-request>
```
The error parameters allow clients to display relevant error messages to users:
| Parameter | Description |
| ------------------- | --------------------------------------------------------------------- |
| `error` | Error code (e.g., `access_denied`, `invalid_request`, `server_error`) |
| `error_description` | Human-readable error description explaining what went wrong |
| `state` | The state parameter from the original request (for CSRF protection) |
### Step 5: Token exchange
The client exchanges the authorization code for tokens by making a POST request to the token endpoint:
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=<authorization-code>' \
-d 'client_id=<client-id>' \
-d 'redirect_uri=<redirect-uri>' \
-d 'code_verifier=<code-verifier>'
```
For confidential clients (with client secret):
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=<authorization-code>' \
-d 'client_id=<client-id>' \
-d 'client_secret=<client-secret>' \
-d 'redirect_uri=<redirect-uri>' \
-d 'code_verifier=<code-verifier>'
```
#### Example in JavaScript
```javascript
// Retrieve the code verifier from storage
const codeVerifier = sessionStorage.getItem('code_verifier')
// Exchange code for tokens
const response = await fetch(`https://<project-ref>.supabase.co/auth/v1/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
client_id: '<client-id>',
redirect_uri: '<redirect-uri>',
code_verifier: codeVerifier,
}),
})
const tokens = await response.json()
```
### Step 6: Token response
On success, Supabase Auth returns a JSON response with tokens:
```json
{
"access_token": "eyJhbGc...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "MXff...",
"scope": "openid email profile",
"id_token": "eyJhbGc..."
}
```
| Field | Description |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| `access_token` | JWT access token for accessing resources |
| `token_type` | Always `bearer` |
| `expires_in` | Token lifetime in seconds (default: 3600) |
| `refresh_token` | Token for obtaining new access tokens |
| `scope` | Granted scopes from the authorization request |
| `id_token` | OpenID Connect ID token (included only if `openid` scope was requested in the authorization request) |
## Access token structure
Access tokens are JWTs containing standard Supabase claims plus OAuth-specific claims:
```json
{
"aud": "authenticated",
"exp": 1735819200,
"iat": 1735815600,
"iss": "https://<project-ref>.supabase.co/auth/v1",
"sub": "user-uuid",
"email": "user@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {},
"role": "authenticated",
"aal": "aal1",
"amr": [
{
"method": "password",
"timestamp": 1735815600
}
],
"session_id": "session-uuid",
"client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d"
}
```
### OAuth-specific claims
| Claim | Description |
| ----------- | -------------------------------------------- |
| `client_id` | The OAuth client ID that obtained this token |
All other claims follow the standard [Supabase JWT structure](/docs/guides/auth/jwts).
### Available scopes
The following scopes are currently supported:
| Scope | Description |
| --------- | ------------------------------------------------------------------------------------- |
| `openid` | Enables OpenID Connect. When requested, an ID token will be included in the response. |
| `email` | Grants access to email and email_verified claims |
| `profile` | Grants access to profile information (name, picture, etc.) |
| `phone` | Grants access to phone_number and phone_number_verified claims |
**Default scope:** When no scope is specified in the authorization request, the default scope is `email`.
Scopes affect what information is included in ID tokens and returned by the UserInfo endpoint. All OAuth access tokens have full access to user data (same as regular session tokens), with the addition of the `client_id` claim. Use Row Level Security policies with the `client_id` claim to control which data each OAuth client can access.
<Admonition type="note">
**Custom scopes are not currently supported.** Only the standard scopes listed above are available. Support for custom scopes is planned for a future release, which will allow you to define application-specific permissions and fine-grained access control.
</Admonition>
## Refresh token flow
Refresh tokens allow clients to obtain new access tokens without requiring the user to re-authenticate.
### When to refresh
Clients should refresh access tokens when:
- The access token is expired (check the `exp` claim)
- The access token is about to expire (proactive refresh)
- An API call returns a 401 Unauthorized error
### Refresh request
Make a POST request to the token endpoint with the refresh token:
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=<refresh-token>' \
-d 'client_id=<client-id>'
```
For confidential clients:
```bash
curl -X POST 'https://<project-ref>.supabase.co/auth/v1/oauth/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=<refresh-token>' \
-d 'client_id=<client-id>' \
-d 'client_secret=<client-secret>'
```
#### Example in JavaScript
```javascript
async function refreshAccessToken(refreshToken) {
const response = await fetch(`https://<project-ref>.supabase.co/auth/v1/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: '<client-id>',
}),
})
if (!response.ok) {
throw new Error('Failed to refresh token')
}
return await response.json()
}
```
### Refresh response
The response contains a new access token and optionally a new refresh token:
```json
{
"access_token": "eyJhbGc...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "v1.MXff...",
"scope": "openid email profile"
}
```
<Admonition type="note">
Refresh tokens may be rotated (a new refresh token is issued). Always update your stored refresh token when a new one is provided.
</Admonition>
## OpenID Connect (OIDC)
Supabase Auth supports OpenID Connect, an identity layer on top of OAuth 2.1.
<Admonition type="note">
**ID tokens are only included when the `openid` scope is requested.** To receive an ID token, include `openid` in the space-separated list of scopes in your authorization request. ID tokens are valid for 1 hour.
</Admonition>
### ID tokens
ID tokens are JWTs that contain user identity information. They are signed by Supabase Auth and can be verified by clients.
The claims included in the ID token depend on the scopes requested during authorization. For example, requesting `openid email profile` will include email and profile-related claims, while requesting only `openid email` will include only email-related claims.
#### Example ID token
```json
{
"iss": "https://<project-ref>.supabase.co/auth/v1",
"sub": "user-uuid",
"aud": "client-id",
"exp": 1735819200,
"iat": 1735815600,
"auth_time": 1735815600,
"nonce": "random-nonce-from-request",
"email": "user@example.com",
"email_verified": true,
"phone_number": "+1234567890",
"phone_number_verified": false,
"name": "John Doe",
"picture": "https://example.com/avatar.jpg"
}
```
#### Standard OIDC claims
| Claim | Description |
| ----------------------- | ------------------------------------------------------------ |
| `sub` | Subject (user ID) |
| `nonce` | The nonce value from the authorization request (if provided) |
| `email` | User's email address |
| `email_verified` | Whether the email is verified |
| `phone_number` | User's phone number |
| `phone_number_verified` | Whether the phone is verified |
| `name` | User's full name |
| `picture` | User's profile picture URL |
### UserInfo endpoint
Clients can retrieve user information by calling the UserInfo endpoint with an access token:
```bash
curl 'https://<project-ref>.supabase.co/auth/v1/oauth/userinfo' \
-H 'Authorization: Bearer <access-token>'
```
The information returned depends on the scopes granted in the access token. For example:
**With `email` scope:**
```json
{
"sub": "user-uuid",
"email": "user@example.com",
"email_verified": true
}
```
**With `email profile phone` scopes:**
```json
{
"sub": "user-uuid",
"email": "user@example.com",
"email_verified": true,
"phone_number": "+1234567890",
"phone_number_verified": false,
"name": "John Doe",
"picture": "https://example.com/avatar.jpg"
}
```
### OIDC discovery
Supabase Auth exposes OpenID Connect and OAuth 2.1 discovery endpoints that describe its capabilities:
```
https://<project-ref>.supabase.co/auth/v1/.well-known/openid-configuration
https://<project-ref>.supabase.co/auth/v1/.well-known/oauth-authorization-server
```
<Admonition type="note">
Both endpoints return the same metadata and can be used interchangeably. They are provided for compatibility with different OAuth and OIDC clients that may expect one or the other.
</Admonition>
These endpoints return metadata about:
- Available endpoints (authorization, token, userinfo, JWKS)
- Supported grant types and response types
- Supported scopes and claims
- Token signing algorithms
This enables automatic integration with OIDC-compliant libraries and tools.
## Token validation
Third-party clients should validate access tokens to ensure they're authentic and not tampered with.
<Admonition type="tip">
**Recommended: Use asymmetric JWT signing keys**
For OAuth implementations, we strongly recommend using asymmetric signing algorithms (RS256 or ES256) instead of the default HS256. With asymmetric keys, third-party clients can validate JWTs using the public key from your JWKS endpoint without needing access to your JWT secret. This is more secure, scalable, and follows OAuth best practices.
Learn how to [configure asymmetric JWT signing keys](/docs/guides/auth/signing-keys) in your project.
</Admonition>
<Admonition type="caution">
**ID tokens require asymmetric signing algorithms**
If you request the `openid` scope to receive ID tokens, your project must be configured to use asymmetric signing algorithms (RS256 or ES256). ID token generation will fail with an error if your project is still using the default HS256 symmetric algorithm. This is a security requirement of the OpenID Connect specification.
</Admonition>
### JWKS endpoint
Supabase Auth exposes a JSON Web Key Set (JWKS) endpoint containing public keys for token verification:
```
https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json
```
Example response:
```json
{
"keys": [
{
"kty": "RSA",
"kid": "key-id",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
```
### Validating tokens
Use a JWT library to verify tokens:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="node"
queryGroup="language"
>
<TabPanel id="node" label="Node.js">
```javascript
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json')
)
async function verifyAccessToken(token) {
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://<project-ref>.supabase.co/auth/v1',
audience: 'authenticated',
})
return payload
} catch (error) {
console.error('Token verification failed:', error)
return null
}
}
```
</TabPanel>
<TabPanel id="python" label="Python">
```python
from jose import jwt
from jose.backends import RSAKey
import requests
# Fetch JWKS
jwks = requests.get('https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json').json()
def verify_access_token(token):
try:
payload = jwt.decode(
token,
jwks,
algorithms=['RS256'],
issuer='https://<project-ref>.supabase.co/auth/v1',
audience='authenticated'
)
return payload
except jwt.JWTError as e:
print(f'Token verification failed: {e}')
return None
```
</TabPanel>
<TabPanel id="go" label="Go">
```go
package main
import (
"context"
"github.com/coreos/go-oidc/v3/oidc"
)
func verifyAccessToken(ctx context.Context, token string) (*oidc.IDToken, error) {
provider, err := oidc.NewProvider(
ctx,
"https://<project-ref>.supabase.co/auth/v1",
)
if err != nil {
return nil, err
}
verifier := provider.Verifier(&oidc.Config{
ClientID: "authenticated",
})
return verifier.Verify(ctx, token)
}
```
</TabPanel>
</Tabs>
### What to validate
Always verify:
1. **Signature** - Token is signed by Supabase Auth
2. **Issuer** (`iss`) - Matches your project URL
3. **Audience** (`aud`) - Is `authenticated`
4. **Expiration** (`exp`) - Token is not expired
5. **Client ID** (`client_id`) - Matches your client (if applicable)
## Managing user grants
Users can view and manage the OAuth applications they've authorized to access their account. This is important for transparency and security, allowing users to audit and revoke access when needed.
### Viewing authorized applications
Users can retrieve a list of all OAuth clients they've authorized:
```javascript
const { data: grants, error } = await supabase.auth.oauth.getUserGrants()
if (error) {
console.error('Error fetching grants:', error)
} else {
console.log('Authorized applications:', grants)
}
```
The response includes details about each authorized OAuth client:
```json
[
{
"id": "grant-uuid",
"client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"client_name": "My Third-Party App",
"scopes": ["email", "profile"],
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
]
```
### Revoking access
Users can revoke access for a specific OAuth client at any time. When access is revoked, all active sessions and refresh tokens for that client are immediately invalidated:
```javascript
const { error } = await supabase.auth.oauth.revokeGrant(clientId)
if (error) {
console.error('Error revoking access:', error)
} else {
console.log('Access revoked successfully')
}
```
After revoking access:
- All refresh tokens for that client are deleted
- The user will need to re-authorize the application to grant access again
<Admonition type="tip">
**Build a settings page for your users**
It's a good practice to provide a settings page where users can view all authorized applications and revoke access to any they no longer trust or use. This increases transparency and gives users control over their data.
</Admonition>
For complete API reference, see the [OAuth methods in supabase-js](/docs/reference/javascript/auth-oauth).
## Next steps
- [Implement MCP authentication](/docs/guides/auth/oauth-server/mcp-authentication) - Enable AI agent authentication
- [Secure with RLS](/docs/guides/auth/oauth-server/token-security) - Control data access for OAuth clients
- [Learn about JWTs](/docs/guides/auth/jwts) - Understand Supabase token structure
@@ -0,0 +1,446 @@
---
id: 'oauth-server-token-security'
title: 'Token Security and Row Level Security'
description: 'Secure your data with Row Level Security policies for OAuth clients'
---
When you enable OAuth 2.1 in your Supabase project, third-party applications can access user data on their behalf. Row Level Security (RLS) policies are crucial for controlling exactly what data each OAuth client can access.
<Admonition type="caution">
**Scopes control OIDC data, not database access**
The OAuth scopes (`openid`, `email`, `profile`, `phone`) control what user information is included in ID tokens and returned by the UserInfo endpoint. They do **not** control access to your database tables or API endpoints.
Use RLS to define which OAuth clients can access which data, regardless of the scopes they requested.
</Admonition>
## How OAuth tokens work with RLS
OAuth access tokens issued by Supabase Auth are JWTs that include all standard Supabase claims plus OAuth-specific claims. This means your existing RLS policies continue to work, and you can add OAuth-specific logic to create granular access controls.
### Token structure
Every OAuth access token includes:
```json
{
"sub": "user-uuid",
"role": "authenticated",
"aud": "authenticated",
"user_id": "user-uuid",
"email": "user@example.com",
"client_id": "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"aal": "aal1",
"amr": [{ "method": "password", "timestamp": 1735815600 }],
"session_id": "session-uuid",
"iss": "https://<project-ref>.supabase.co/auth/v1",
"iat": 1735815600,
"exp": 1735819200
}
```
The key OAuth-specific claim is:
| Claim | Description |
| ----------- | -------------------------------------------------------------- |
| `client_id` | Unique identifier of the OAuth client that obtained this token |
You can use this claim in RLS policies to grant different permissions to different clients.
## Extracting OAuth claims in RLS
Use the `auth.jwt()` function to access token claims in your policies:
```sql
-- Get the client ID from the token
(auth.jwt() ->> 'client_id')
-- Check if the token is from an OAuth client
(auth.jwt() ->> 'client_id') IS NOT NULL
-- Check if the token is from a specific client
(auth.jwt() ->> 'client_id') = 'mobile-app-client-id'
```
## Common RLS patterns for OAuth
### Pattern 1: Grant specific client full access
Allow a specific OAuth client to access all user data:
```sql
CREATE POLICY "Mobile app can access user data"
ON user_data FOR ALL
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') = 'mobile-app-client-id'
);
```
### Pattern 2: Grant multiple clients read-only access
Allow several OAuth clients to read data, but not modify it:
```sql
CREATE POLICY "Third-party apps can read profiles"
ON profiles FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') IN (
'analytics-client-id',
'reporting-client-id',
'dashboard-client-id'
)
);
```
### Pattern 3: Restrict sensitive data from OAuth clients
Prevent OAuth clients from accessing sensitive data:
```sql
CREATE POLICY "OAuth clients cannot access payment info"
ON payment_methods FOR ALL
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') IS NULL -- Only direct user sessions
);
```
### Pattern 4: Client-specific data access
Different clients access different subsets of data:
```sql
-- Analytics client can only read aggregated data
CREATE POLICY "Analytics client reads summaries"
ON user_metrics FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') = 'analytics-client-id'
);
-- Admin client can read and modify all data
CREATE POLICY "Admin client full access"
ON user_data FOR ALL
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') = 'admin-client-id'
);
```
## Real-world examples
### Example 1: Multi-platform application
You have a web app, mobile app, and third-party integrations:
```sql
-- Web app: Full access
CREATE POLICY "Web app full access"
ON profiles FOR ALL
USING (
auth.uid() = user_id AND
(
(auth.jwt() ->> 'client_id') = 'web-app-client-id'
OR (auth.jwt() ->> 'client_id') IS NULL -- Direct user sessions
)
);
-- Mobile app: Read-only access to profiles
CREATE POLICY "Mobile app reads profiles"
ON profiles FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') = 'mobile-app-client-id'
);
-- Third-party integration: Limited data access
CREATE POLICY "Integration reads public data"
ON profiles FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') = 'integration-client-id' AND
is_public = true
);
```
## Custom access token hooks
[Custom Access Token Hooks](/docs/guides/auth/auth-hooks/custom-access-token-hook) work with OAuth tokens, allowing you to inject custom claims based on the OAuth client. This is particularly useful for customizing standard JWT claims like `audience` (`aud`) or adding client-specific metadata.
<Admonition type="note">
Custom Access Token Hooks are triggered for **all** token issuance. Use `client_id` or `authentication_method` (`oauth_provider/authorization_code` for OAuth flows) to differentiate OAuth from regular authentication.
</Admonition>
### Customizing the audience claim
A common use case is customizing the `audience` claim for different OAuth clients. This allows third-party services to validate that tokens were issued specifically for them:
```typescript
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
const { user, claims, client_id } = await req.json()
// Customize audience based on OAuth client
if (client_id === 'mobile-app-client-id') {
return new Response(
JSON.stringify({
claims: {
aud: 'https://api.myapp.com',
app_version: '2.0.0',
},
}),
{ headers: { 'Content-Type': 'application/json' } }
)
}
if (client_id === 'analytics-partner-id') {
return new Response(
JSON.stringify({
claims: {
aud: 'https://analytics.partner.com',
access_level: 'read-only',
},
}),
{ headers: { 'Content-Type': 'application/json' } }
)
}
// Default audience for non-OAuth flows
return new Response(JSON.stringify({ claims: {} }), {
headers: { 'Content-Type': 'application/json' },
})
})
```
The `audience` claim is especially important for:
- **JWT validation by third parties**: Services can verify tokens were issued for their specific API
- **Multi-tenant applications**: Different audiences for different client applications
- **Compliance**: Meeting security requirements that mandate audience validation
### Adding client-specific claims
You can also add custom claims and metadata based on the OAuth client:
```typescript
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const { user, claims, client_id } = await req.json()
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SECRET_KEY')!)
// Add custom claims based on OAuth client
let customClaims = {}
if (client_id === 'mobile-app-client-id') {
customClaims.aud = 'https://mobile.myapp.com'
customClaims.app_version = '2.0.0'
customClaims.platform = 'mobile'
} else if (client_id === 'analytics-client-id') {
customClaims.aud = 'https://analytics.myapp.com'
customClaims.read_only = true
customClaims.data_retention_days = 90
} else if (client_id?.startsWith('mcp-')) {
// MCP AI agents
const { data: agent } = await supabase
.from('approved_ai_agents')
.select('name, max_data_retention_days')
.eq('client_id', client_id)
.single()
customClaims.aud = `https://mcp.myapp.com/${client_id}`
customClaims.ai_agent = true
customClaims.agent_name = agent?.name
customClaims.max_retention = agent?.max_data_retention_days
}
return new Response(JSON.stringify({ claims: customClaims }), {
headers: { 'Content-Type': 'application/json' },
})
})
```
Use these custom claims in RLS:
```sql
-- Policy based on custom claims
CREATE POLICY "Read-only clients cannot modify"
ON user_data FOR UPDATE
USING (
auth.uid() = user_id AND
(auth.jwt() -> 'user_metadata' ->> 'read_only')::boolean IS NOT TRUE
);
-- Policy based on audience claim
CREATE POLICY "Only specific audience can access"
ON api_data FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'aud') IN (
'https://api.myapp.com',
'https://mobile.myapp.com'
)
);
```
## Security best practices
### 1. Principle of least privilege
Grant OAuth clients only the minimum permissions they need:
```sql
-- Bad: Grant all access by default
CREATE POLICY "OAuth clients full access"
ON user_data FOR ALL
USING (auth.uid() = user_id);
-- Good: Grant specific access per client
CREATE POLICY "Specific client specific access"
ON user_data FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') = 'trusted-client-id'
);
```
### 2. Separate policies for OAuth clients
Create dedicated policies for OAuth clients rather than mixing them with user policies:
```sql
-- User access
CREATE POLICY "Users access their own data"
ON user_data FOR ALL
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') IS NULL
);
-- OAuth client access (separate policy)
CREATE POLICY "OAuth clients limited access"
ON user_data FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') IN ('client-1', 'client-2')
);
```
### 3. Regularly audit OAuth clients
Track and review which clients have access:
```sql
-- View all active OAuth clients
SELECT
oc.client_id,
oc.name,
oc.created_at,
COUNT(DISTINCT s.user_id) as active_users
FROM auth.oauth_clients oc
LEFT JOIN auth.sessions s ON s.client_id = oc.client_id
WHERE s.created_at > NOW() - INTERVAL '30 days'
GROUP BY oc.client_id, oc.name, oc.created_at;
```
## Testing your policies
Always test your RLS policies before deploying to production:
```sql
-- Test as a specific OAuth client
SET request.jwt.claims = '{
"sub": "test-user-uuid",
"role": "authenticated",
"client_id": "test-client-id"
}';
-- Test queries
SELECT * FROM user_data WHERE user_id = 'test-user-uuid';
-- Reset
RESET request.jwt.claims;
```
Or use the Supabase Dashboard's [RLS policy tester](/dashboard/project/_/auth/policies).
## Troubleshooting
### Policy not working for OAuth client
**Problem**: OAuth client can't access data despite having a valid token.
**Check**:
1. Verify the policy includes the client's `client_id`
2. Ensure RLS is enabled on the table
3. Check for conflicting restrictive policies
4. Test with service role key to isolate RLS issues
```sql
-- Debug: See what client_id is in the token
SELECT auth.jwt() ->> 'client_id';
-- Debug: Test without RLS
SET LOCAL role = service_role;
SELECT * FROM your_table;
```
### Policy too permissive
**Problem**: OAuth client has access to data it shouldn't.
**Solution**: Use `AS RESTRICTIVE` policies to add additional constraints:
```sql
-- This policy runs in addition to permissive policies
CREATE POLICY "Restrict OAuth clients"
ON sensitive_data
AS RESTRICTIVE
FOR ALL
TO authenticated
USING (
-- OAuth clients cannot access this table at all
(auth.jwt() ->> 'client_id') IS NULL
);
```
### Can't differentiate between users and OAuth clients
**Problem**: Need to apply different logic for direct user sessions vs OAuth.
**Solution**: Check if `client_id` is present:
```sql
-- Direct user sessions (no OAuth)
CREATE POLICY "Direct users full access"
ON user_data FOR ALL
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') IS NULL
);
-- OAuth clients (limited access)
CREATE POLICY "OAuth clients read only"
ON user_data FOR SELECT
USING (
auth.uid() = user_id AND
(auth.jwt() ->> 'client_id') IS NOT NULL
);
```
## Next steps
- [Learn about JWTs](/docs/guides/auth/jwts) - Deep dive into Supabase token structure
- [Row Level Security](/docs/guides/auth/row-level-security) - Complete RLS guide
- [Custom Access Token Hooks](/docs/guides/auth/auth-hooks/custom-access-token-hook) - Inject custom claims
- [OAuth flows](/docs/guides/auth/oauth-server/oauth-flows) - Understand token issuance
@@ -9,6 +9,14 @@ subtitle: 'Set up redirect urls with Supabase Auth.'
Supabase Auth allows you to control how the [user sessions](/docs/guides/auth/sessions) are handled by your application.
<Admonition type="note">
**Looking for OAuth client redirect URIs?**
This guide covers redirect URLs for users signing **into** your application (using social providers like Google, GitHub, etc.). If you're setting up your Supabase project as an **OAuth 2.1 provider** for third-party applications, see the [OAuth Server Redirect URI configuration](/docs/guides/auth/oauth-server/getting-started#redirect-uri-configuration) instead.
</Admonition>
When using [passwordless sign-ins](/docs/reference/javascript/auth-signinwithotp) or [third-party providers](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect), the Supabase client library provides a `redirectTo` parameter to specify where to redirect the user after authentication. The URL in `redirectTo` should match the [Redirect URLs](/dashboard/project/_/auth/url-configuration) list configuration.
To configure allowed redirect URLs, go to the [URL Configuration](/dashboard/project/_/auth/url-configuration) page. Once you've added necessary URLs, you can use the URL you want the user to be redirected to in the `redirectTo` parameter.
@@ -1,7 +1,7 @@
---
id: 'replication'
title: 'Database Replication'
description: 'Replicate your database to external destinations using ETL or manual replication.'
description: 'Replicate your database to external destinations using Supabase replication or manual replication.'
subtitle: 'An introduction to database replication and change data capture.'
sidebar_label: 'Overview'
---
@@ -20,17 +20,17 @@ You might use database replication for:
Supabase supports two methods for replicating your database to external destinations:
### ETL replication
### Replication
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
Use Supabase ETL to automatically replicate data to supported systems.
Replication powered by [Supabase ETL](https://github.com/supabase/etl) automatically replicates data to supported systems.
- [Set up ETL Replication](/docs/guides/database/replication/etl-replication-setup)
- [Set up replication](/docs/guides/database/replication/replication-setup)
### Manual replication
@@ -1,126 +0,0 @@
---
id: 'etl-bigquery'
title: 'ETL to BigQuery'
description: 'Replicate your Supabase database to Google BigQuery using ETL Replication.'
subtitle: 'Stream data changes to BigQuery in real-time.'
sidebar_label: 'BigQuery'
---
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
BigQuery is Google's fully managed data warehouse. ETL Replication allows you to automatically sync your Supabase database tables to BigQuery for analytics and reporting.
<Admonition type="tip">
This page covers BigQuery-specific configuration. For complete setup instructions including publications, general settings, and pipeline management, see the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup).
</Admonition>
### Setup
Setting up BigQuery replication requires preparing your GCP resources, then configuring BigQuery as an ETL destination.
#### Step 1: Prepare GCP resources
Before configuring BigQuery as a destination, set up the following in Google Cloud Platform:
1. **Google Cloud Platform (GCP) account**: [Sign up for GCP](https://cloud.google.com/gcp) if you don't have one
2. **BigQuery dataset**: Create a [BigQuery dataset](https://cloud.google.com/bigquery/docs/datasets-intro) in your GCP project
3. **GCP service account key**: Create a [service account](https://cloud.google.com/iam/docs/keys-create-delete) with the **BigQuery Data Editor** role and download the JSON key file
#### Step 2: Add BigQuery as an ETL destination
After preparing your GCP resources, configure BigQuery as an ETL destination:
1. Navigate to [Database](/dashboard/project/_/database/etl) → **ETL Replication** in your Supabase Dashboard
2. Click **Add destination**
3. Configure the destination:
<Image
alt="BigQuery Configuration Settings"
src="/docs/img/database/replication/etl-bigquery-details.png"
zoomable
/>
- **Destination type**: Select **BigQuery**
- **Project ID**: Your BigQuery project identifier (found in the GCP Console)
- **Dataset ID**: The name of your BigQuery dataset (without the project ID)
<Admonition type="note">
In the GCP Console, the dataset is shown as `project-id.dataset-id`. Enter only the part after the dot. For example, if you see `my-project.my_dataset`, enter `my_dataset`.
</Admonition>
- **Service Account Key**: Your GCP service account key in JSON format. The service account must have the following permissions:
- `bigquery.datasets.get`
- `bigquery.tables.create`
- `bigquery.tables.get`
- `bigquery.tables.getData`
- `bigquery.tables.update`
- `bigquery.tables.updateData`
4. Complete the remaining configuration following the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup)
### How it works
Once configured, ETL Replication to BigQuery:
1. Captures changes from your Postgres database (INSERT, UPDATE, DELETE operations)
2. Batches changes for optimal performance
3. Creates BigQuery tables automatically to match your Postgres schema
4. Streams data to BigQuery with CDC metadata
<Admonition type="note">
Due to ingestion latency in BigQuery's streaming API, there may be a delay (typically seconds to minutes) in data appearing. This is normal and expected for BigQuery's architecture.
</Admonition>
#### BigQuery CDC format
BigQuery tables include additional columns for change tracking:
- `_change_type`: The type of change (`INSERT`, `UPDATE`, `DELETE`)
- `_commit_timestamp`: When the change was committed in Postgres
- `_stream_id`: Internal identifier for the replication stream
### Querying replicated data
Once replication is running, you can query your data in BigQuery:
```sql
-- Query the replicated table
SELECT * FROM `your-project.your_dataset.users`
WHERE created_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY);
-- View CDC changes
SELECT
_change_type,
_commit_timestamp,
id,
name,
email
FROM `your-project.your_dataset.users`
ORDER BY _commit_timestamp DESC
LIMIT 100;
```
### Limitations
BigQuery-specific limitations:
- **Ingestion latency**: BigQuery's streaming API has inherent latency (typically seconds to minutes)
- **Row size**: Limited to 10 MB per row due to BigQuery Storage Write API constraints
For general ETL Replication limitations that apply to all destinations, see the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup#limitations).
### Next steps
- [Set up ETL Replication](/docs/guides/database/replication/etl-replication-setup)
- [Monitor ETL Replication](/docs/guides/database/replication/etl-replication-monitoring)
- [View ETL Replication FAQ](/docs/guides/database/replication/etl-replication-faq)
@@ -1,32 +0,0 @@
---
id: 'etl-destinations'
title: 'ETL Destinations'
description: 'Choose where to replicate your database with ETL Replication.'
subtitle: 'Available destinations for ETL Replication.'
sidebar_label: 'Destinations'
---
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
ETL Replication supports multiple destination types for syncing your database. Choose the destination that best fits your analytics and integration needs.
<Admonition type="note">
Some destinations may not be available for all users. Additional destinations are planned for the future, but we don't have public timelines to share at this time.
</Admonition>
### Available destinations
| Destination | Description | Configuration |
| ------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------- |
| **Iceberg (Analytics Buckets)** | Apache Iceberg tables in S3-compatible storage | [Configure Iceberg →](/docs/guides/database/replication/etl-iceberg) |
### Next steps
- [Set up ETL Replication](/docs/guides/database/replication/etl-replication-setup)
- [Monitor ETL Replication](/docs/guides/database/replication/etl-replication-monitoring)
@@ -1,88 +0,0 @@
---
id: 'etl-iceberg'
title: 'ETL to Iceberg (Analytics Buckets)'
description: 'Replicate your Supabase database to Iceberg format using Analytics Buckets.'
subtitle: 'Stream data to Analytics Buckets.'
sidebar_label: 'Iceberg'
---
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
<Admonition type="caution" label="Current Limitation">
Iceberg replication is currently incomplete. It provides an append-only log listing all your data changes with an additional column explaining the type of operation (INSERT, UPDATE, DELETE).
</Admonition>
Apache Iceberg is an open table format for analytic datasets. ETL Replication to Iceberg uses Supabase [Analytics Buckets](/docs/guides/storage/analytics) to store your replicated data.
<Admonition type="tip">
This page covers Iceberg-specific configuration. For complete setup instructions including publications, general settings, and pipeline management, see the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup).
</Admonition>
### Setup
Setting up Iceberg replication requires two steps: creating an Analytics Bucket, then configuring it as an ETL destination.
#### Step 1: Create an Analytics bucket
First, create an Analytics Bucket to store your replicated data:
1. Navigate to [Storage](/dashboard/project/_/storage/buckets) → **Analytics** in your Supabase Dashboard
2. Click **New bucket**
<Image
alt="Create New Analytics Bucket"
src="/docs/img/database/replication/etl-iceberg-new-bucket.png"
zoomable
/>
#### Step 2: Add Iceberg as an ETL destination
After clicking **New bucket**, fill in the bucket details and copy the credentials:
1. Fill in the bucket details:
<Image
alt="Analytics Bucket Details"
src="/docs/img/database/replication/etl-iceberg-details.png"
zoomable
/>
- **Name**: A unique name for your bucket
- **Region**: Select the region where your data will be stored
2. Click **Create bucket**
3. **Copy the credentials** displayed after bucket creation (Catalog Token, S3 Access Key ID, S3 Secret Access Key). You'll need these in the next steps.
4. Navigate to [Database](/dashboard/project/_/database/etl) → **ETL Replication** in your Supabase Dashboard
5. Click **Add destination**
6. Configure the destination:
- **Destination type**: Select **Iceberg (Analytics Bucket)**
- **Bucket**: The name of your Analytics Bucket from Step 1
- **Namespace**: The schema name where your tables will be replicated (e.g., `public`)
- **Catalog Token**: Authentication token for accessing the Iceberg catalog (copied in Step 3)
- **S3 Access Key ID**: Access key for S3-compatible storage (copied in Step 3)
- **S3 Secret Access Key**: Secret key for S3-compatible storage (copied in Step 3)
7. Complete the remaining configuration following the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup)
For more information about Analytics Buckets, see the [Analytics Buckets documentation](/docs/guides/storage/analytics).
### Limitations
Iceberg-specific limitations:
- **Append-only log**: Currently provides an append-only log format rather than a full table representation
For general ETL Replication limitations that apply to all destinations, see the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup#limitations).
### Next steps
- [Set up ETL Replication](/docs/guides/database/replication/etl-replication-setup)
- [Monitor ETL Replication](/docs/guides/database/replication/etl-replication-monitoring)
- [View ETL Replication FAQ](/docs/guides/database/replication/etl-replication-faq)
@@ -1,100 +0,0 @@
---
id: 'etl-replication-faq'
title: 'ETL Replication FAQ'
description: 'Frequently asked questions about ETL replication.'
subtitle: 'Common questions and answers about ETL replication.'
sidebar_label: 'FAQ'
---
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
## What destinations are supported?
ETL Replication currently supports Iceberg (Analytics Buckets). See the [ETL Destinations guide](/docs/guides/database/replication/etl-destinations) for details.
## Why is a table not being replicated?
Common reasons:
- **Missing primary key**: Tables must have a primary key to be replicated
- **Not in publication**: Ensure the table is included in your publication
- **Unsupported data types**: Tables with custom data types are not supported
- **Partitioned tables**: Not currently supported
Check your publication settings and verify your table meets the requirements.
## Why aren't publication changes reflected after adding or removing tables?
After modifying your publication, you must restart the ETL pipeline for changes to take effect. See [Adding or removing tables](/docs/guides/database/replication/etl-replication-setup#adding-or-removing-tables) for instructions.
## Why is a pipeline in failed state?
Pipeline failures occur during the streaming phase when an error happens while replicating live data. This prevents data loss. To recover:
1. Check the error message by hovering over the **Failed** status
2. Click **View status** for detailed information
3. Fix the underlying issue (e.g., schema mismatches, destination connectivity)
4. Restart the pipeline
See [Handling errors](/docs/guides/database/replication/etl-replication-monitoring#handling-errors) for more details.
## Why is a table in error state?
Table errors occur during the copy phase. To recover, click **View status**, find the affected table, and reset the table state. This will restart the table copy from the beginning.
## How to verify replication is working
Check the [Database](/dashboard/project/_/database/etl) → **ETL Replication** page:
1. Verify your pipeline shows **Running** status
2. Click **View status** to check table states
3. Ensure all tables show **Live** state (actively replicating)
4. Monitor replication lag metrics
See the [ETL Replication Monitoring guide](/docs/guides/database/replication/etl-replication-monitoring) for comprehensive monitoring instructions.
## What are the main limitations?
Key limitations to be aware of:
- **Primary keys**: Required on all tables
- **Custom data types**: Not supported
- **Schema changes**: Not automatically handled
Destination-specific limitations may also apply. See the [Iceberg](/docs/guides/database/replication/etl-iceberg#limitations) destination page for details.
## How to stop or pause replication
You can manage your pipeline using the actions menu in the destinations list. See [Managing your pipeline](/docs/guides/database/replication/etl-replication-setup#managing-your-pipeline) for details on available actions.
Note: Stopping replication will cause changes to queue up in the WAL.
## Can data duplicates occur during pipeline operations?
Yes, data duplicates can occur in certain scenarios when stopping a pipeline.
When you stop a pipeline (for restarts or updates), ETL Replication tries to finish processing any transactions that are currently being sent to your destination. It waits up to a few minutes to allow these in-progress transactions to complete cleanly before stopping.
However, if a transaction in your database takes longer than this waiting period to complete, the pipeline will stop before that entire transaction has been fully processed. When the pipeline starts again, it must restart the incomplete transaction from the beginning to maintain transaction boundaries, which results in some data being sent twice to your destination.
**Understanding transaction boundaries**: A transaction is a group of database changes that happen together (for example, all changes within a `BEGIN...COMMIT` block). ETL Replication must process entire transactions - it cannot process part of a transaction, stop, and then continue from the middle. This means if a transaction is interrupted, the whole transaction must be replayed when the pipeline resumes.
**Example scenario**: Suppose you have a batch operation that updates 10,000 rows within a single transaction. If this operation takes 10 minutes to complete and you stop the pipeline after 5 minutes (when 5,000 rows have been processed), the pipeline cannot resume from row 5,001. Instead, when it restarts, it must reprocess all 10,000 rows from the beginning, resulting in the first 5,000 rows being sent to your destination twice.
**Important**: We are not currently planning to implement automatic deduplication. If your use case requires guaranteed exactly-once delivery, you should implement deduplication logic in your downstream systems based on primary keys or other unique identifiers.
## Where to find replication logs
Navigate to [Logs](/dashboard/project/_/logs/explorer) → **ETL Replication** to see all pipeline logs. Logs contain diagnostic information. If you're experiencing issues, contact support with your error details.
## How to get help
If you need assistance:
1. Check the [ETL Replication Setup guide](/docs/guides/database/replication/etl-replication-setup) and [ETL Replication Monitoring guide](/docs/guides/database/replication/etl-replication-monitoring)
2. Review this FAQ for common issues
3. Contact support with your error details and logs
@@ -1,260 +0,0 @@
---
id: 'etl-replication-setup'
title: 'ETL Replication Setup'
description: 'Set up ETL replication to automatically sync your database to external destinations.'
subtitle: 'Configure publications and destinations for ETL replication.'
sidebar_label: 'Setting up'
---
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
ETL Replication requires two main components: a **publication** (source) and a **destination**. Follow these steps to set up your replication pipeline.
<Admonition type="tip">
If you already have a publication set up, you can skip to [Step 2: Enable ETL Replication](#step-2-enable-etl-replication).
</Admonition>
### Step 1: Create a publication
A publication defines which tables and change types will be replicated. You need to create a publication using SQL.
#### Creating a publication
The following SQL examples assume you have `users` and `orders` tables in your database.
##### Publication for specific tables
```sql
-- Create publication for both tables
create publication pub_users_orders
for table users, orders;
```
This publication will track all changes (INSERT, UPDATE, DELETE) for both the `users` and `orders` tables.
##### Publication for all tables in a schema
```sql
-- Create a publication for all tables in the public schema
create publication pub_all_public for tables in schema public;
```
This will track changes for all existing and future tables in the `public` schema.
##### Publication for all tables
```sql
-- Create a publication for all tables
create publication pub_all_tables for all tables;
```
This will track changes for all tables in your database.
#### Advanced publication options
##### Selecting specific columns
You can replicate only a subset of columns from a table:
```sql
-- Replicate only specific columns from the users table
create publication pub_users_subset
for table users (id, email, created_at);
```
This will only replicate the `id`, `email`, and `created_at` columns from the `users` table.
##### Filtering rows with a predicate
You can filter which rows to replicate using a WHERE clause:
```sql
-- Only replicate active users
create publication pub_active_users
for table users where (status = 'active');
-- Only replicate recent orders
create publication pub_recent_orders
for table orders where (created_at > '2024-01-01');
```
#### Viewing publications in the Dashboard
After creating a publication via SQL, you can view it in the Supabase Dashboard:
1. Navigate to **Database** → [Publications](/dashboard/project/_/database/publications) in your Supabase Dashboard
2. You'll see all your publications listed with their tables
### Step 2: Enable ETL replication
Before adding destinations, you need to enable ETL replication for your project:
1. Navigate to the [Database](/dashboard/project/_/database/etl) section in your Supabase Dashboard
2. Select the **ETL Replication** tab
3. Click **Enable replication** to activate ETL replication for your project
<Image
alt="Enable ETL Replication"
src="/docs/img/database/replication/etl-enable-replication.png"
zoomable
/>
### Step 3: Add a destination
Once replication is enabled and you have a publication, you can add a destination. The destination is where your replicated data will be stored, while the pipeline is the active process that continuously replicates changes from your database to that destination.
#### Available destinations
For a complete list of available destinations and how to choose the right one for your needs, see [ETL Destinations](/docs/guides/database/replication/etl-destinations).
#### Configuration
1. In the ETL Replication tab, click **Add destination**
2. Configure the destination settings:
**General Settings:**
- **Destination name**: A name to identify this destination (e.g., "Analytics Warehouse")
- **Publication**: The publication to replicate data from (created in Step 1)
- **Destination type**: Choose from available destination types
**Destination-specific settings:**
Each destination type requires different configuration. See the [ETL Destinations guide](/docs/guides/database/replication/etl-destinations) for configuration details specific to your chosen destination.
<Image
alt="Add ETL Destination"
src="/docs/img/database/replication/etl-add-destination.png"
zoomable
/>
3. Configure **Advanced Settings** (optional):
- **Batch wait time (milliseconds)**: How long to wait for more changes before sending a batch. We recommend leaving this at the default value for optimal performance. Setting this too low can result in too much traffic and less efficient batching.
4. Click **Create and start** to begin replication
### Step 4: Monitor your pipeline
After creating a destination, the pipeline will start and appear in the destinations list. You can monitor the pipeline's status and performance from the Dashboard.
<Image
alt="ETL Destinations List"
src="/docs/img/database/replication/etl-destinations-list.png"
zoomable
/>
For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [ETL Replication Monitoring guide](/docs/guides/database/replication/etl-replication-monitoring).
### Managing your pipeline
You can manage your pipeline from the destinations list using the actions menu.
<Image
alt="Pipeline Actions"
src="/docs/img/database/replication/etl-pipeline-actions.png"
zoomable
/>
Available actions:
- **Start**: Begin replication for a stopped pipeline
- **Stop**: Pause replication (changes will queue up in the WAL)
- **Restart**: Stop and start the pipeline (required after publication changes)
- **Edit destination**: Modify destination settings like credentials or advanced options
- **Delete**: Remove the destination and permanently stop replication
### Adding or removing tables
If you need to modify which tables are replicated after your ETL pipeline is already running, follow these steps:
<Admonition type="note">
If your publication uses `FOR ALL TABLES` or `FOR TABLES IN SCHEMA`, new tables in that scope are automatically included in the publication. However, you still **must restart the ETL pipeline** for the changes to take effect.
</Admonition>
#### Adding tables to replication
1. Add the table to your publication using SQL:
```sql
-- Add a single table to an existing publication
alter publication pub_users_orders add table products;
-- Or add multiple tables at once
alter publication pub_users_orders add table products, categories;
```
2. **Restart the ETL pipeline** using the actions menu (see [Managing your pipeline](#managing-your-pipeline)) for the changes to take effect.
#### Removing tables from replication
1. Remove the table from your publication using SQL:
```sql
-- Remove a single table from a publication
alter publication pub_users_orders drop table orders;
-- Or remove multiple tables at once
alter publication pub_users_orders drop table orders, products;
```
2. **Restart the ETL pipeline** using the actions menu (see [Managing your pipeline](#managing-your-pipeline)) for the changes to take effect.
### How it works
Once configured, ETL Replication:
1. **Captures** changes from your database using the publication
2. **Loads** the data to your destination in near real-time batches
Changes are sent in batches to optimize performance and reduce costs. The batch size and timing can be adjusted using the advanced settings.
<Admonition type="note">
ETL Replication currently performs data extraction and loading only, without transformation. Your data is replicated as-is to the destination.
</Admonition>
### Troubleshooting
If you encounter issues during setup:
- **Publication not appearing**: Ensure you created the publication via SQL and refresh the dashboard
- **Tables not showing in publication**: Verify your tables have primary keys (required for replication)
- **Pipeline failed to start**: Check the error message in the status view for specific details
- **No data being replicated**: Verify your publication includes the correct tables and event types
For more troubleshooting help, see the [ETL Replication FAQ](/docs/guides/database/replication/etl-replication-faq).
### Limitations
ETL Replication has the following limitations that apply to all destinations:
- **Primary keys required**: Tables must have primary keys
- **Custom data types**: Not supported
- **Schema changes**: Not automatically handled
- **No data transformation**: Data is replicated as-is without transformation
- **Data duplicates**: Duplicates can occur when stopping a pipeline if your database has transactions that take longer than a few minutes to complete. See [Can data duplicates occur?](/docs/guides/database/replication/etl-replication-faq#can-data-duplicates-occur-during-pipeline-operations) for details
Destination-specific limitations may also apply. See the [Iceberg](/docs/guides/database/replication/etl-iceberg#limitations) destination page for details.
### Future work
ETL Replication is actively being developed. Planned improvements include:
- **DDL support**: Automatic handling of schema changes (ALTER TABLE, ADD COLUMN, etc.)
- **Additional destinations**: Support for more data warehouses and analytics platforms
We don't have public timelines for these features, but they represent our roadmap for making ETL Replication more robust and flexible.
### Next steps
- [Monitor ETL Replication](/docs/guides/database/replication/etl-replication-monitoring)
- [View ETL Replication FAQ](/docs/guides/database/replication/etl-replication-faq)
@@ -1,6 +1,6 @@
---
id: 'replication-faq'
title: 'Replication FAQ'
id: 'manual-replication-faq'
title: 'Manual Replication FAQ'
description: 'Frequently asked questions about manual database replication.'
subtitle: 'Common questions and considerations when setting up manual replication.'
sidebar_label: 'FAQ'
@@ -30,13 +30,15 @@ You can view [publications](/dashboard/project/default/database/publications) in
## How to configure database settings for replication?
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.
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 your database size and activity.
## What are some important configuration options?
Some of the more important options to be aware of are:
- `max_wal_size`
- `max_slot_wal_keep_size`
- `wal_keep_size`
- `max_wal_senders`
- `max_wal_size` - Maximum size the WAL can grow between automatic WAL checkpoints
- `max_slot_wal_keep_size` - Maximum size of WAL files that replication slots are allowed to retain
- `wal_keep_size` - Minimum number of past WAL files to keep for standby servers
- `max_wal_senders` - Maximum number of concurrent connections from standby servers or streaming backup clients
These settings help ensure your replication slots don't run out of space and that replicas can reconnect without requiring a full re-sync.
@@ -1,6 +1,6 @@
---
id: 'monitoring-replication'
title: 'Monitoring Replication'
id: 'manual-replication-monitoring'
title: 'Manual Replication Monitoring'
description: 'Monitor replication lag and status for manual replication setups.'
subtitle: 'Track replication health and performance.'
sidebar_label: 'Monitoring'
@@ -16,7 +16,7 @@ Monitoring replication lag is important and there are 3 ways to do this:
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)
- 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 network issues)
### Primary
@@ -51,7 +51,7 @@ The WAL size can be checked using the `pg_ls_waldir()` function:
select * from pg_ls_waldir();
```
#### Check LSN
#### Check the LSN
```sql
select pg_current_wal_lsn();
@@ -91,7 +91,7 @@ ORDER BY
table_name;
```
#### Check LSN
#### Check the LSN
```sql
select pg_last_wal_replay_lsn();
@@ -6,7 +6,7 @@ subtitle: 'Set up replication with Airbyte, Estuary, Fivetran, and other tools.'
sidebar_label: 'Setting up'
---
This guide covers setting up **manual replication** using external tools. If you prefer a simpler, managed solution, see [ETL Replication](/docs/guides/database/replication/etl-replication-setup) instead.
This guide covers setting up **manual replication** using external tools. If you prefer a simpler, managed solution, see [Replication](/docs/guides/database/replication/replication-setup) instead.
<Admonition type="note">
@@ -21,7 +21,7 @@ To set up replication, the following is recommended:
- Instance size of XL or greater
- [IPv4 add-on](/docs/guides/platform/ipv4-address) enabled
To create a replication slot, you will need to use the `postgres` user and follow the instructions in our [guide](/docs/guides/database/postgres/setup-replication-external).
To create a replication slot, you will need to use the `postgres` user and follow the instructions in the [external replication setup guide](/docs/guides/database/postgres/setup-replication-external).
<Admonition type="note">
@@ -29,7 +29,7 @@ If you are running Postgres 17 or higher, you can create a new user and grant th
</Admonition>
If you are replicating to an external system and using any of the tools below, check their documentation first and we have added additional information where the setup with Supabase can vary.
If you are replicating to an external system and using any of the tools below, check their documentation first. Additional information is provided where the setup with Supabase can vary.
<Tabs
scrollable
@@ -67,7 +67,7 @@ You can follow those steps with the following modifications:
1. In Step 2, choose `logical replication` as the sync mechanism
2. In Step 3, do not create a user and use the existing `postgres` user for replication
3. In Step 5, no need to modify any WAL settings as we have done that for you
3. In Step 5, no need to modify any WAL settings as this has been configured
</TabPanel>
@@ -77,7 +77,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](/docs/guides/database/postgres/setup-replication-external) to create a publication slot
1. Follow the steps in the [external replication setup guide](/docs/guides/database/postgres/setup-replication-external) to create a publication slot
</TabPanel>
@@ -0,0 +1,115 @@
---
id: 'replication-faq'
title: 'Replication FAQ'
description: 'Frequently asked questions about replication.'
subtitle: 'Common questions and answers about replication.'
sidebar_label: 'FAQ'
---
<Admonition type="caution" label="Private Alpha">
Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
## What destinations are supported?
Replication currently supports Analytics Buckets (Iceberg format) and BigQuery. See the destination tabs in the [Setup guide](/docs/guides/database/replication/replication-setup#step-3-add-a-destination) for configuration details.
Availability varies based on the planned roll-out strategy. The destinations you can access depend on your project and access level.
## Why is a table not being replicated?
Common reasons:
- **Missing primary key**: Tables must have a primary key to be replicated (Postgres logical replication requirement)
- **Not in publication**: Ensure the table is included in your Postgres publication
- **Unsupported data types**: Tables with custom data types are not supported
Check your publication settings and verify your table meets the requirements.
## Why aren't publication changes reflected after adding or removing tables?
After modifying your Postgres publication, you must restart the replication pipeline for changes to take effect. See [Adding or removing tables](/docs/guides/database/replication/replication-setup#adding-or-removing-tables) for instructions.
## Why is a pipeline in failed state?
Pipeline failures occur during the streaming phase when an error happens while replicating live data. This prevents data loss. To recover:
1. Check the error message by hovering over the **Failed** status
2. Click **View status** for detailed information
3. Fix the underlying issue (e.g., schema mismatches, destination connectivity)
4. Restart the pipeline
See [Handling errors](/docs/guides/database/replication/replication-monitoring#handling-errors) for more details.
## Why is a table in error state?
Table errors occur during the copy phase. To recover, click **View status**, find the affected table, and reset the table state. This will restart the table copy from the beginning.
## How to verify replication is working
Check the [Database](/dashboard/project/_/database/replication) → **replication** page:
1. Verify your pipeline shows **Running** status
2. Click **View status** to check table states
3. Ensure all tables show **Live** state (actively replicating)
4. Monitor replication lag metrics
See the [Replication Monitoring guide](/docs/guides/database/replication/replication-monitoring) for comprehensive monitoring instructions.
## How to stop or pause replication
You can manage your pipeline using the actions menu in the destinations list. See [Managing your pipeline](/docs/guides/database/replication/replication-setup#managing-your-pipeline) for details on available actions.
Note: Stopping replication will cause changes to queue up in the WAL.
## What happens if a table is deleted at the destination?
If a table is deleted downstream at the destination (e.g., in your Analytics Bucket or BigQuery dataset), the replication pipeline will automatically recreate it.
This behavior is by design to prevent the pipeline from breaking if tables are accidentally deleted. The pipeline ensures that all tables in your publication are always present at the destination.
**To permanently remove a table from your destination:**
You have two options:
**Option 1: Pause the pipeline first**
1. Pause or delete your replication pipeline
2. Delete the table at your destination
3. The table will not be recreated since the pipeline is not running
**Option 2: Remove from publication first**
1. Remove the table from your Postgres publication using `ALTER PUBLICATION ... DROP TABLE`
2. Restart your replication pipeline to apply the change (the table at the destination will remain but stop receiving new changes)
3. Delete the table at your destination
Note: Removing a table from the publication and restarting the pipeline does not delete the table downstream, it only stops replicating new changes to it.
## Can data duplicates occur during pipeline operations?
Yes, data duplicates can occur in certain scenarios when stopping a pipeline.
When you stop a pipeline (for restarts or updates), the replication process tries to finish processing any transactions that are currently being sent to your destination. It waits up to a few minutes to allow these in-progress transactions to complete cleanly before stopping.
However, if a transaction in your database takes longer than this waiting period to complete, the pipeline will stop before that entire transaction has been fully processed. When the pipeline starts again, it must restart the incomplete transaction from the beginning to maintain transaction boundaries, which results in some data being sent twice to your destination.
**Understanding transaction boundaries**: A transaction is a group of database changes that happen together (for example, all changes within a `BEGIN...COMMIT` block). Postgres logical replication must process entire transactions - it cannot process part of a transaction, stop, and then continue from the middle. This means if a transaction is interrupted, the whole transaction must be replayed when the pipeline resumes.
**Example scenario**: Suppose you have a batch operation that updates 10,000 rows within a single transaction. If this operation takes 10 minutes to complete and you stop the pipeline after 5 minutes (when 5,000 rows have been processed), the pipeline cannot resume from row 5,001. Instead, when it restarts, it must reprocess all 10,000 rows from the beginning, resulting in the first 5,000 rows being sent to your destination twice.
**Important**: There are currently no plans to implement automatic deduplication. If your use case requires guaranteed exactly-once delivery, you should implement deduplication logic in your downstream systems based on primary keys or other unique identifiers.
## Where to find replication logs
Navigate to [Logs](/dashboard/project/_/logs/explorer) → **Replication** to see all pipeline logs. Logs contain diagnostic information. If you're experiencing issues, contact support with your error details.
## How to get help
If you need assistance:
1. Check the [Replication Setup guide](/docs/guides/database/replication/replication-setup) and [Replication Monitoring guide](/docs/guides/database/replication/replication-monitoring)
2. Review this FAQ for common issues
3. Contact support with your error details and logs
@@ -1,30 +1,30 @@
---
id: 'etl-replication-monitoring'
title: 'ETL Replication Monitoring'
description: 'Monitor the status and health of your ETL replication pipelines.'
id: 'replication-monitoring'
title: 'Replication Monitoring'
description: 'Monitor the status and health of your replication pipelines.'
subtitle: 'Track replication status, view logs, and troubleshoot issues.'
sidebar_label: 'Monitoring'
---
<Admonition type="caution" label="Private Alpha">
ETL Replication is currently in private alpha. Access is limited and features may change.
Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
After setting up ETL replication, you can monitor the status and health of your replication pipelines directly from the Supabase Dashboard. The pipeline is the active process that continuously replicates changes from your database to your destination.
After setting up replication, you can monitor the status and health of your replication pipelines directly from the Supabase Dashboard. The pipeline is the active Postgres replication process that continuously streams changes from your database to your destination.
### Viewing pipeline status
To monitor your ETL replication pipelines:
To monitor your replication pipelines:
1. Navigate to the [Database](/dashboard/project/_/database/etl) section in your Supabase Dashboard
2. Select the **ETL Replication** tab
1. Navigate to the [Database](/dashboard/project/_/database/replication) section in your Supabase Dashboard
2. Select the **replication** tab
3. You'll see a list of all your destinations with their pipeline status
<Image
alt="ETL Destinations List"
src="/docs/img/database/replication/etl-destinations-list.png"
alt="Destinations List"
src="/docs/img/database/replication/replication-destinations-list.png"
zoomable
/>
@@ -46,7 +46,7 @@ For detailed information about a specific pipeline, click **View status** on the
<Image
alt="Pipeline Status View"
src="/docs/img/database/replication/etl-view-status.png"
src="/docs/img/database/replication/replication-view-status.png"
zoomable
/>
@@ -76,7 +76,7 @@ Table errors occur during the copy phase and affect individual tables. These err
<Image
alt="Table Error Details"
src="/docs/img/database/replication/etl-pipeline-table-error.png"
src="/docs/img/database/replication/replication-pipeline-table-error.png"
zoomable
/>
@@ -88,15 +88,21 @@ Table errors occur during the copy phase and affect individual tables. These err
**Recovering from table errors:**
When a table encounters an error during the copy phase, you can reset the table state (for some errors). This will restart the table copy from the beginning.
When a table encounters an error during the copy phase, you can reset the table state. This will restart the table copy from the beginning.
#### Pipeline errors
Pipeline errors occur during the streaming phase (Live state) and affect the entire pipeline. When streaming data, if an error occurs, the entire pipeline will stop and enter a **Failed** state. This prevents data loss by ensuring no changes are skipped.
<Admonition type="note">
When a pipeline error occurs, you'll receive an email notification immediately. This ensures you're promptly notified of any issues so you can take action to resolve them.
</Admonition>
<Image
alt="Pipeline Error Details"
src="/docs/img/database/replication/etl-pipeline-error.png"
src="/docs/img/database/replication/replication-pipeline-error.png"
zoomable
/>
@@ -104,7 +110,7 @@ Pipeline errors occur during the streaming phase (Live state) and affect the ent
1. Hover over the **Failed** status in the destinations list to see a quick error summary
2. Click **View status** for comprehensive error information
3. Navigate to [Logs](/dashboard/project/_/logs/explorer) → **ETL Replication** for detailed error logs
3. Navigate to [Logs](/dashboard/project/_/logs/explorer) → **Replication** for detailed error logs
**Recovering from pipeline errors:**
@@ -116,21 +122,17 @@ To recover from a pipeline error, you'll need to:
### Viewing logs
To see detailed logs for all your ETL replication pipelines:
To see detailed logs for all your replication pipelines:
1. Navigate to [Logs](/dashboard/project/_/logs/explorer) in your Supabase Dashboard
2. Select **ETL Replication** from the log source filter
2. Select **Replication** from the log source filter
3. You'll see all logs from your replication pipelines
<Image
alt="ETL Replication Logs"
src="/docs/img/database/replication/etl-replication-logs.png"
zoomable
/>
<Image alt="Replication Logs" src="/docs/img/database/replication/replication-logs.png" zoomable />
<Admonition type="note">
Logs contain diagnostic information that may be too technical for most users. If you're experiencing issues with ETL replication, reaching out to support with your error details is recommended.
Logs contain diagnostic information that may be too technical for most users. If you're experiencing issues with replication, reaching out to support with your error details is recommended.
</Admonition>
@@ -138,7 +140,7 @@ Logs contain diagnostic information that may be too technical for most users. If
#### Checking if replication is healthy
1. Navigate to [Database](/dashboard/project/_/database/etl) → **ETL Replication**
1. Navigate to [Database](/dashboard/project/_/database/replication) → **replication**
2. Verify your destination shows **Running** status
3. Click **View status** to check replication lag and table states
4. Ensure all tables show **Live** state
@@ -150,7 +152,7 @@ If you see a **Failed** status:
1. Hover over the status to see the error summary
2. Click **View status** to see detailed error information
3. Check table states to identify which tables are affected
4. Navigate to [Logs](/dashboard/project/_/logs/explorer) → **ETL Replication** for full error details
4. Navigate to [Logs](/dashboard/project/_/logs/explorer) → **Replication** for full error details
5. For table errors, attempt to reset the affected tables
#### Monitoring performance
@@ -164,17 +166,17 @@ To ensure optimal performance:
### Troubleshooting
If you notice issues with your ETL replication:
If you notice issues with your replication:
1. **Check pipeline state**: Ensure the pipeline is in **Running** state
2. **Review table states**: Identify tables in **Error** state
3. **Check logs**: Navigate to Logs → ETL Replication for detailed error information
4. **Verify publication**: Ensure your publication is properly configured
3. **Check logs**: Navigate to Logs → Replication for detailed error information
4. **Verify publication**: Ensure your Postgres publication is properly configured
5. **Monitor replication lag**: High lag may indicate performance issues
For more troubleshooting tips, see the [ETL Replication FAQ](/docs/guides/database/replication/etl-replication-faq).
For more troubleshooting tips, see the [Replication FAQ](/docs/guides/database/replication/replication-faq).
### Next steps
- [Set up ETL Replication](/docs/guides/database/replication/etl-replication-setup)
- [View ETL Replication FAQ](/docs/guides/database/replication/etl-replication-faq)
- [Set up replication](/docs/guides/database/replication/replication-setup)
- [View replication FAQ](/docs/guides/database/replication/replication-faq)
@@ -0,0 +1,445 @@
---
id: 'replication-setup'
title: 'Replication Setup'
description: 'Set up replication using Postgres logical replication.'
subtitle: 'Configure publications and destinations for replication.'
sidebar_label: 'Setting up'
---
<Admonition type="caution" label="Private Alpha">
Replication is currently in private alpha. Access is limited and features may change.
</Admonition>
Replication uses **Postgres logical replication** to stream changes from your database. Powered by [Supabase ETL](https://github.com/supabase/etl), an open source tool built for Postgres logical replication, it provides a managed interface through the Dashboard to configure and monitor replication pipelines.
## Setup overview
Replication requires two main components: a **Postgres publication** (defines what to replicate) and a **destination** (where data is sent). Follow these steps to set up your replication pipeline.
<Admonition type="tip">
If you already have a Postgres publication set up, you can skip to [Step 2: Enable replication](#step-2-enable-replication).
</Admonition>
### Step 1: Create a Postgres publication
A Postgres publication defines which tables and change types will be replicated from your database. You create publications using SQL.
#### Creating a publication
The following SQL examples assume you have `users` and `orders` tables in your database.
##### Publication for specific tables
```sql
-- Create publication for both tables
create publication pub_users_orders
for table users, orders;
```
This publication will track all changes (INSERT, UPDATE, DELETE, TRUNCATE) for both the `users` and `orders` tables.
##### Publication for all tables in a schema
```sql
-- Create a publication for all tables in the public schema
create publication pub_all_public for tables in schema public;
```
This will track changes for all existing and future tables in the `public` schema.
##### Publication for all tables
```sql
-- Create a publication for all tables
create publication pub_all_tables for all tables;
```
This will track changes for all tables in your database.
#### Advanced publication options
##### Selecting specific columns
You can replicate only a subset of columns from a table:
```sql
-- Replicate only specific columns from the users table
create publication pub_users_subset
for table users (id, email, created_at);
```
This will only replicate the `id`, `email`, and `created_at` columns from the `users` table.
##### Filtering rows with a predicate
You can filter which rows to replicate using a WHERE clause:
```sql
-- Only replicate active users
create publication pub_active_users
for table users where (status = 'active');
-- Only replicate recent orders
create publication pub_recent_orders
for table orders where (created_at > '2024-01-01');
```
#### Viewing publications in the Dashboard
After creating a publication via SQL, you can view it in the Supabase Dashboard:
1. Navigate to **Database** → [Publications](/dashboard/project/_/database/publications) in your Supabase Dashboard
2. You'll see all your publications listed with their tables
### Step 2: Enable replication
Before adding destinations, you need to enable replication for your project:
1. Navigate to the [Database](/dashboard/project/_/database/replication) section in your Supabase Dashboard
2. Select the **replication** tab
3. Click **Enable replication** to activate replication for your project
<Image
alt="Enable Replication"
src="/docs/img/database/replication/replication-enable-replication.png"
zoomable
/>
### Step 3: Add a destination
Once replication is enabled and you have a Postgres publication, you can add a destination. The destination is where your replicated data will be stored, while the pipeline is the active Postgres replication process that continuously streams changes from your database to that destination.
#### Choose and configure your destination
Follow these steps to configure your destination. The specific configuration depends on which destination type you choose. Both Analytics Buckets and BigQuery destinations are supported, though availability varies based on the planned roll-out strategy.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="analytics-buckets"
queryGroup="destination"
>
<TabPanel id="analytics-buckets" label="Analytics Buckets">
[Analytics Buckets](/docs/guides/storage/analytics/introduction) are specialized storage buckets in Supabase Storage designed for analytical workloads. They provide S3-compatible storage and use the [Apache Iceberg](https://iceberg.apache.org/) open table format, making your data accessible via standard tools like DuckDB, Spark, and other analytics platforms.
When you replicate to Analytics Buckets, your database changes are automatically written in Iceberg format, creating tables in object storage that you can query for analytics.
##### Step 1: Create an analytics bucket
First, create an analytics bucket to store your replicated data:
1. Navigate to [Storage](/dashboard/project/_/storage/buckets) → **Analytics** in your Supabase Dashboard
2. Click **New bucket**
<Image
alt="Create new analytics bucket"
src="/docs/img/database/replication/replication-iceberg-new-bucket.png"
zoomable
/>
3. Fill in the bucket details:
<Image
alt="Analytics bucket details"
src="/docs/img/database/replication/replication-iceberg-details.png"
zoomable
/>
- **Name**: A unique name for your bucket (e.g., `analytics_warehouse`)
- **Region**: Select the region where your data will be stored
4. Click **Create bucket**
5. **Copy the credentials** displayed after bucket creation. You'll need these in the next steps:
- **Catalog Token**: Authentication token for accessing the Iceberg catalog
- **S3 Access Key ID**: Access key for S3-compatible storage
- **S3 Secret Access Key**: Secret key for S3-compatible storage
##### Step 2: Configure analytics buckets as a destination
1. Navigate to [Database](/dashboard/project/_/database/replication) → **replication** in your Supabase Dashboard
2. Click **Add destination**
<Image
alt="Add Destination"
src="/docs/img/database/replication/replication-add-destination.png"
zoomable
/>
3. Configure the general settings:
- **Destination name**: A name to identify this destination (e.g., "Analytics Warehouse")
- **Publication**: The publication to replicate data from (created in [Step 1](#step-1-create-a-postgres-publication))
- **Destination type**: Select **Analytics Buckets**
4. Configure Analytics Buckets settings:
- **Bucket**: The name of your analytics bucket from Step 1
- **Namespace**: The schema name where your tables will be replicated (e.g., `public`)
- **Catalog Token**: Authentication token from Step 1
- **S3 Access Key ID**: Access key from Step 1
- **S3 Secret Access Key**: Secret key from Step 1
5. Configure **Advanced Settings** (optional):
- **Batch wait time (milliseconds)**: How long to wait for more changes before sending a batch. Default is recommended for optimal performance.
6. Click **Create and start** to begin replication
Your replication pipeline will now start copying data from your database to the analytics bucket in Iceberg format.
##### How it works
Once configured, replication to Analytics Buckets:
1. Captures changes from your Postgres database (INSERT, UPDATE, DELETE, TRUNCATE operations)
2. Batches changes for optimal performance
3. Creates Iceberg tables automatically to match your Postgres schema
4. Streams data to Analytics Buckets
##### How tables are structured
Replicated tables use a changelog structure:
- Tables are created with a `_changelog` suffix in the name (e.g., `users_changelog`)
- Each table contains a `cdc_operation` column indicating the operation type: `INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`
- This append-only format preserves the complete history of all changes
##### Limitations
- **Append-only log**: Currently provides an append-only log format rather than a full table representation
##### Additional resources
- [Analytics Buckets documentation](/docs/guides/storage/analytics/introduction) - Learn more about Analytics Buckets and S3-compatible storage
- [Realtime Data Sync to Analytics Buckets](/docs/guides/storage/analytics/replication) - Step-by-step guide for replicating to Analytics Buckets
- [Apache Iceberg](https://iceberg.apache.org/) - Official Iceberg documentation
</TabPanel>
<TabPanel id="bigquery" label="BigQuery">
[BigQuery](https://cloud.google.com/bigquery) is Google's fully managed data warehouse. You can replicate your database tables to BigQuery for analytics and reporting.
##### Step 1: Prepare GCP resources
Before configuring BigQuery as a destination, set up the following in Google Cloud Platform:
1. **Google Cloud Platform (GCP) account**: [Sign up for GCP](https://cloud.google.com/gcp) if you don't have one
2. **BigQuery dataset**: Create a [BigQuery dataset](https://cloud.google.com/bigquery/docs/datasets-intro) in your GCP project
- Open the BigQuery console in GCP
- Select your project
- Click "Create Dataset"
- Provide a dataset ID (e.g., `supabase_replication`)
3. **GCP service account key**: Create a [service account](https://cloud.google.com/iam/docs/keys-create-delete) with appropriate permissions:
- Go to IAM & Admin → Service Accounts
- Click "Create Service Account"
- Grant the **BigQuery Data Editor** role
- Create and download the JSON key file
Required permissions:
- `bigquery.datasets.get`
- `bigquery.tables.create`
- `bigquery.tables.get`
- `bigquery.tables.getData`
- `bigquery.tables.update`
- `bigquery.tables.updateData`
##### Step 2: Configure BigQuery as a destination
1. Navigate to [Database](/dashboard/project/_/database/replication) → **replication** in your Supabase Dashboard
2. Click **Add destination**
<Image
alt="BigQuery Configuration Settings"
src="/docs/img/database/replication/replication-bigquery-details.png"
zoomable
/>
3. Configure the general settings:
- **Destination name**: A name to identify this destination (e.g., "BigQuery Warehouse")
- **Publication**: The publication to replicate data from (created in [Step 1](#step-1-create-a-postgres-publication))
- **Destination type**: Select **BigQuery**
4. Configure BigQuery-specific settings:
- **Project ID**: Your BigQuery project identifier (found in the GCP Console)
- **Dataset ID**: The name of your BigQuery dataset (without the project ID)
<Admonition type="note">
In the GCP Console, the dataset is shown as `project-id.dataset-id`. Enter only the part after the dot. For example, if you see `my-project.my_dataset`, enter `my_dataset`.
</Admonition>
- **Service Account Key**: Your GCP service account key in JSON format (from Step 1)
5. Configure **Advanced Settings** (optional):
- **Batch wait time (milliseconds)**: How long to wait for more changes before sending a batch. Default is recommended for optimal performance.
6. Click **Create and start** to begin replication
Your replication pipeline will now start copying data from your database to BigQuery.
##### How it works
Once configured, replication to BigQuery:
1. Captures changes from your Postgres database (INSERT, UPDATE, DELETE, TRUNCATE operations)
2. Batches changes for optimal performance
3. Creates BigQuery tables automatically to match your Postgres schema
4. Streams data to BigQuery
##### How tables are structured
Due to BigQuery limitations, replicated tables use a versioned structure:
- The table you query is a **view** (e.g., `users`)
- The actual data is stored in versioned tables with a `_version` suffix (e.g., `users_version`)
- When a table is truncated in your database, a new version is created and the view automatically points to the latest version
This structure handles table truncations seamlessly while maintaining query compatibility.
##### Limitations
- **Row size**: Limited to 10 MB per row due to BigQuery Storage Write API constraints
##### Additional resources
- [BigQuery documentation](https://cloud.google.com/bigquery/docs) - Official Google BigQuery documentation
</TabPanel>
</Tabs>
### Step 4: Monitor your pipeline
After creating a destination, the replication pipeline will start and appear in the destinations list. You can monitor the pipeline's status and performance from the Dashboard.
<Image
alt="Replication Destinations List"
src="/docs/img/database/replication/replication-destinations-list.png"
zoomable
/>
For comprehensive monitoring instructions including pipeline states, metrics, and logs, see the [Replication Monitoring guide](/docs/guides/database/replication/replication-monitoring).
### Managing your pipeline
You can manage your pipeline from the destinations list using the actions menu.
<Image
alt="Pipeline Actions"
src="/docs/img/database/replication/replication-pipeline-actions.png"
zoomable
/>
Available actions:
- **Start**: Begin replication for a stopped pipeline
- **Stop**: Pause replication (changes will queue up in the WAL)
- **Restart**: Stop and start the pipeline (required after publication changes)
- **Edit destination**: Modify destination settings like credentials or advanced options
- **Delete**: Remove the destination and permanently stop replication
### Adding or removing tables
If you need to modify which tables are replicated after your replication pipeline is already running, follow these steps:
<Admonition type="note">
If your Postgres publication uses `FOR ALL TABLES` or `FOR TABLES IN SCHEMA`, new tables in that scope are automatically included in the publication. However, you still **must restart the replication pipeline** for the changes to take effect.
</Admonition>
#### Adding tables to replication
1. Add the table to your publication using SQL:
```sql
-- Add a single table to an existing publication
alter publication pub_users_orders add table products;
-- Or add multiple tables at once
alter publication pub_users_orders add table products, categories;
```
2. **Restart the replication pipeline** using the actions menu (see [Managing your pipeline](#managing-your-pipeline)) for the changes to take effect.
#### Removing tables from replication
1. Remove the table from your Postgres publication using SQL:
```sql
-- Remove a single table from a publication
alter publication pub_users_orders drop table orders;
-- Or remove multiple tables at once
alter publication pub_users_orders drop table orders, products;
```
2. **Restart the replication pipeline** using the actions menu (see [Managing your pipeline](#managing-your-pipeline)) for the changes to take effect.
<Admonition type="note">
Deleted tables are automatically recreated by the pipeline. To permanently delete a table, pause the pipeline first or remove it from the publication before deleting. See the [FAQ](/docs/guides/database/replication/replication-faq#what-happens-if-a-table-is-deleted-at-the-destination) for details.
</Admonition>
### How it works
Once configured, replication:
1. **Captures** changes from your Postgres database using Postgres publications and logical replication
2. **Streams** the changes through the replication pipeline
3. **Loads** the data to your destination in near real-time batches
Changes are sent in batches to optimize performance and reduce costs. The batch size and timing can be adjusted using the advanced settings. The replication pipeline currently performs data extraction and loading only, without transformation - your data is replicated as-is to the destination.
### Troubleshooting
If you encounter issues during setup:
- **Publication not appearing**: Ensure you created the Postgres publication via SQL and refresh the dashboard
- **Tables not showing in publication**: Verify your tables have primary keys (required for Postgres logical replication)
- **Pipeline failed to start**: Check the error message in the status view for specific details
- **No data being replicated**: Verify your Postgres publication includes the correct tables and event types
For more troubleshooting help, see the [Replication FAQ](/docs/guides/database/replication/replication-faq).
### Limitations
Replication has the following limitations:
- **Primary keys required**: Tables must have primary keys (Postgres logical replication requirement)
- **Custom data types**: Not supported
- **Schema changes**: Not automatically handled
- **No data transformation**: Data is replicated as-is without transformation
- **Data duplicates**: Duplicates can occur when stopping a pipeline if your database has transactions that take longer than a few minutes to complete. See [Can data duplicates occur?](/docs/guides/database/replication/replication-faq#can-data-duplicates-occur-during-pipeline-operations) for details
Destination-specific limitations (such as Iceberg's append-only log format or BigQuery's row size limits) are detailed in each destination tab in [Step 3](#step-3-add-a-destination) above.
### Future work
Replication is actively being developed. Planned improvements include:
- **DDL support**: Automatic handling of Postgres schema changes (ALTER TABLE, ADD COLUMN, etc.)
- **Additional destinations**: Support for more data warehouses and analytics platforms
There are no public timelines for these features, but they represent the roadmap for making replication more robust and flexible.
### Next steps
- [Monitor Replication](/docs/guides/database/replication/replication-monitoring)
- [View Replication FAQ](/docs/guides/database/replication/replication-faq)
@@ -32,9 +32,9 @@ Send database changes to any external service using Webhooks. [Docs](/docs/guide
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Docs](/docs/guides/database/vault).
### ETL replication
### Replication
Automatically replicate your database to external destinations like data warehouses and analytics platforms. [Docs](/docs/guides/database/replication/etl-replication).
Automatically replicate your database to external destinations like data warehouses and analytics platforms. [Docs](/docs/guides/database/replication/replication-setup).
## Platform
@@ -207,7 +207,7 @@ In addition to the Beta requirements, features in GA are covered by the [uptime
| Database | Auto-generated GraphQL API | `GA` | ✅ |
| Database | Webhooks | `beta` | ✅ |
| Database | Vault | `public alpha` | ✅ |
| Database | ETL Replication | `private alpha` | N/A |
| Database | Replication | `private alpha` | N/A |
| Platform | | `GA` | ✅ |
| Platform | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
| Platform | Custom Domains | `GA` | N/A |
@@ -46,5 +46,5 @@ if (error) {
Once you've created your analytics bucket, you can:
- [Connect with Iceberg clients](/docs/guides/storage/analytics/connecting-to-analytics-bucket) like PyIceberg or Apache Spark
- [Set up real-time replication](/docs/guides/storage/analytics/etl) from your Postgres database
- [Set up real-time replication](/docs/guides/storage/analytics/replication) from your Postgres database
- [Query data with Postgres](/docs/guides/storage/analytics/query-with-postgres) using the Iceberg Foreign Data Wrapper
@@ -3,7 +3,7 @@ title: 'Query with PostgreSQL'
subtitle: 'Query analytics bucket data directly from PostgreSQL using SQL.'
---
Once your data flows into an analytics bucket—either via the [ETL Replication Pipeline](/docs/guides/storage/analytics/etl) or custom pipelines—you can query it directly from Postgres using standard SQL.
Once your data flows into an analytics bucket—either via the [Replication Pipeline](/docs/guides/storage/analytics/replication) or custom pipelines—you can query it directly from Postgres using standard SQL.
This is made possible by the [Iceberg Foreign Data Wrapper](/docs/guides/database/extensions/wrappers/iceberg), which creates a bridge between your Postgres database and Iceberg tables.
@@ -5,15 +5,17 @@ subtitle: 'Replicate your PostgreSQL data to analytics buckets in real-time.'
<Admonition type="caution" title="This feature is in alpha">
Expect rapid changes, limited features, and possible breaking updates. [Share feedback](https://github.com/orgs/supabase/discussions/40116) as we refine the experience and expand access.
Expect rapid changes, limited features, and possible breaking updates. [Share feedback](https://github.com/orgs/supabase/discussions/40116) as the experience is refined and access is expanded.
</Admonition>
By combining Supabase's **ETL service** with **Analytics Buckets**, you can build an end-to-end data warehouse solution that automatically syncs changes from your Postgres database to Iceberg tables.
By combining replication powered by [Supabase ETL](https://github.com/supabase/etl) with **Analytics Buckets**, you can build an end-to-end data warehouse solution that automatically syncs changes from your Postgres database to Iceberg tables.
This guide provides a quickstart for replicating to Analytics Buckets. For complete replication configuration including other destinations, see the [Replication Setup Guide](/docs/guides/database/replication/replication-setup).
## How it works
The ETL pipeline captures changes (INSERT, UPDATE, DELETE) from your Postgres database in real-time and writes them to your analytics bucket. This allows you to maintain an always-up-to-date data warehouse without impacting your production workloads.
The replication pipeline captures changes (INSERT, UPDATE, DELETE) from your Postgres database in real-time using Postgres logical replication and writes them to your analytics bucket. This allows you to maintain an always-up-to-date data warehouse without impacting your production workloads.
## Setup steps
@@ -29,7 +31,7 @@ First, create a new analytics bucket to store your replicated data:
<Image
alt="Creating a new analytics bucket"
src="/docs/img/database/replication/etl-iceberg-new-bucket.png"
src="/docs/img/database/replication/replication-iceberg-new-bucket.png"
zoomable
/>
@@ -43,9 +45,9 @@ CREATE PUBLICATION pub_warehouse
FOR TABLE users, orders, products;
```
This publication will track all changes (INSERT, UPDATE, DELETE) for the specified tables. For advanced use cases, see the [Replication Configuration Guide](/docs/guides/database/replication/etl-replication-setup).
This publication will track all changes (INSERT, UPDATE, DELETE) for the specified tables. For advanced publication options like column filtering and row predicates, see the [Replication Setup Guide](/docs/guides/database/replication/replication-setup#advanced-publication-options).
### Step 3: Create the ETL pipeline
### Step 3: Create the replication pipeline
Now set up the pipeline to sync data to your analytics bucket:
@@ -57,8 +59,8 @@ Now set up the pipeline to sync data to your analytics bucket:
6. Click **Create and Start**.
<Image
alt="ETL pipeline configuration"
src="/docs/img/database/replication/etl-iceberg-details.png"
alt="Replication pipeline configuration"
src="/docs/img/database/replication/replication-iceberg-details.png"
zoomable
/>
@@ -70,6 +72,12 @@ Once started, you can monitor the pipeline status directly in the **Database > R
- **Sync Progress** - View the number of records replicated
- **Logs** - Check detailed logs for troubleshooting
<Admonition type="note">
Deleted tables are automatically recreated by the pipeline. To permanently delete a table, pause the pipeline first or remove it from the publication before deleting. See the [FAQ](/docs/guides/database/replication/replication-faq#what-happens-if-i-delete-a-table-at-the-destination) for details.
</Admonition>
## Next steps
Once data is flowing to your analytics bucket, you can:
@@ -78,4 +86,8 @@ Once data is flowing to your analytics bucket, you can:
- [Connect with PyIceberg](/docs/guides/storage/analytics/examples/pyiceberg)
- [Analyze with Apache Spark](/docs/guides/storage/analytics/examples/apache-spark)
For advanced topics, see the [ETL Replication Monitoring Guide](/docs/guides/database/replication/etl-replication-monitoring).
For detailed replication configuration and advanced topics:
- [Replication Setup Guide](/docs/guides/database/replication/replication-setup) - Complete replication configuration including BigQuery and other destinations
- [Replication Monitoring Guide](/docs/guides/database/replication/replication-monitoring) - Monitor replication pipeline status and health
- [Replication FAQ](/docs/guides/database/replication/replication-faq) - Common questions about replication
-37
View File
@@ -1,37 +0,0 @@
---
slug: /
sidebar_position: 1
id: management-api
title: Management API
sidebar_label: Management API
---
The Management API allows you to manage your projects programmatically.
## Authentication
All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization: Bearer <supabase_personal_token>`.
To generate or manage your API token, visit your [account](https://supabase.com/dashboard/account/tokens) page.
Your API tokens carry the same privileges as your user account, so be sure to keep it secret.
```bash
$ curl https://api.supabase.com/v1/projects \
-H "Authorization: Bearer sbp_bdd0••••••••••••••••••••••••••••••••4f23"
```
All API requests must be authenticated and made over HTTPS.
## Rate limits
The rate limit for Management API is 120 requests per one minute per user, and applies cumulatively across all requests made with your personal access tokens.
If you exceed this limit, all Management API calls for the next minute will be blocked, resulting in a HTTP 429 response.
The Management API is subject to our fair-use policy.
All resources created via the API are subject to the pricing detailed on our [Pricing](https://supabase.com/pricing) pages.
## Additional links
- [OpenAPI Docs](https://api.supabase.com/api/v1)
- [OpenAPI Spec](https://api.supabase.com/api/v1-json)
- Reporting bugs and issues: [github.com/supabase/supabase](https://github.com/supabase/supabase)
+3 -8
View File
@@ -36,16 +36,11 @@ hideTitle: true
-H "Authorization: Bearer sbp_bdd0••••••••••••••••••••••••••••••••4f23"
```
All API requests must be authenticated and made over HTTPS.
All API requests must be authenticated and made over HTTPS foo.
## Rate limits
<$Partial path="api_rate_limits.mdx" />
The rate limit for Management API is 120 requests per one minute per user, and applies cumulatively across all requests made with your personal access tokens.
If you exceed this limit, all Management API calls for the next minute will be blocked, resulting in a HTTP 429 response.
The Management API is subject to our fair-use policy.
All resources created via the API are subject to the pricing detailed on our [Pricing](https://supabase.com/pricing) pages.
The Management API is subject to our fair-use policy. All resources created via the API are subject to the pricing detailed on our [Pricing](https://supabase.com/pricing) pages.
</RefSubLayout.Details>
+20 -28
View File
@@ -19,7 +19,11 @@ import {
ScrollArea,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { McpConfigPanel as McpConfigPanelBase, type McpClient } from 'ui-patterns/McpUrlBuilder'
import {
createMcpCopyHandler,
McpConfigPanel as McpConfigPanelBase,
type McpClient,
} from 'ui-patterns/McpUrlBuilder'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { useDebounce } from '~/hooks/useDebounce'
import { useIntersectionObserver } from '~/hooks/useIntersectionObserver'
@@ -268,34 +272,22 @@ export function McpConfigPanel() {
const isPlatform = selectedPlatform === 'hosted'
const project = isPlatform ? selectedProject : null
const handleCopy = (type?: 'url' | 'json' | 'command') => {
let connectionType: string
switch (type) {
case 'command':
connectionType = 'Command Line'
break
case 'json':
connectionType = 'JSON'
break
case 'url':
default:
connectionType = 'MCP URL'
break
}
sendTelemetryEvent({
action: 'connection_string_copied',
properties: {
connectionTab: 'MCP',
selectedItem: selectedClient?.label,
connectionType,
const handleCopy = useMemo(
() =>
createMcpCopyHandler({
selectedClient,
source: 'docs',
},
groups: {
...(project?.ref && { project: project.ref }),
} as any,
})
}
onTrack: (event) => {
sendTelemetryEvent({
action: event.action,
properties: event.properties,
groups: (event.groups || {}) as any,
})
},
projectRef: project?.ref,
}),
[selectedClient, sendTelemetryEvent, project?.ref]
)
const handleInstall = () => {
if (selectedClient?.label) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

@@ -417,8 +417,8 @@ export const RateLimits = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for sign ups and sign ins"
description="Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users)"
label="Rate limit for sign-ups and sign-ins"
description="Number of sign-up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users)"
>
<Tooltip>
<TooltipTrigger asChild>
@@ -460,8 +460,8 @@ export const RateLimits = () => {
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for Web3 sign up and sign-in"
description="Number of Web3 (Sign in with Solana) sign up or sign in requests that can be made per IP address in 5 minutes"
label="Rate limit for Web3 sign-ups and sign-ins"
description="Number of Web3 sign-up or sign-in requests that can be made per IP address in 5 minutes"
>
<Tooltip>
<TooltipTrigger asChild>
@@ -25,6 +25,7 @@ import {
Input_Shadcn_,
PrePostTab,
Switch,
cn,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
@@ -444,20 +445,45 @@ export const SmtpForm = () => {
</>
)}
<CardFooter className="justify-end space-x-2">
<CardFooter
className={cn(form.formState.isDirty ? 'justify-between' : 'justify-end', 'gap-x-2')}
>
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
<p className="text-sm text-foreground-light">
{enableSmtp ? (
<>
Rate limit for sending emails will be increased to 30 and{' '}
<InlineLink href={`/project/${projectRef}/auth/rate-limits`}>
can be adjusted
</InlineLink>{' '}
after enabling custom SMTP
</>
) : (
'Rate limit for sending emails will be reduced to 2 after disabling custom SMTP'
)}
</p>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdatingConfig}
disabled={!canUpdateConfig || !form.formState.isDirty}
>
Save changes
</Button>
<div className="flex items-center gap-x-2">
{form.formState.isDirty && (
<Button
type="default"
onClick={() => {
form.reset()
setEnableSmtp(isSmtpEnabled(authConfig))
}}
>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdatingConfig}
disabled={!canUpdateConfig || !form.formState.isDirty}
>
Save changes
</Button>
</div>
</CardFooter>
</Card>
</form>
@@ -20,9 +20,9 @@ import { User, useUsersInfiniteQuery } from 'data/auth/users-infinite-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
import { cleanPointerEventsNoneOnBody, isAtBottom } from 'lib/helpers'
import { parseAsArrayOf, parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'
import {
@@ -365,7 +365,7 @@ export const UsersV2 = () => {
isCountLoaded &&
isCountWithinThresholdForSortBy
) {
if (specificFilterColumn === 'id' && localStorageFilter !== 'id') {
if (specificFilterColumn === 'email' && localStorageFilter !== 'email') {
setSpecificFilterColumn(localStorageFilter)
}
if (sortByValue === 'id:asc' && localStorageSortByValue !== 'id:asc') {
@@ -4,8 +4,8 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { BASE_PATH } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { useTheme } from 'next-themes'
import { useState } from 'react'
import { McpConfigPanel, type McpClient } from 'ui-patterns/McpUrlBuilder'
import { useMemo, useState } from 'react'
import { createMcpCopyHandler, McpConfigPanel, type McpClient } from 'ui-patterns/McpUrlBuilder'
import type { projectKeys } from './Connect.types'
export const McpTabContent = ({ projectKeys }: { projectKeys: projectKeys }) => {
@@ -42,28 +42,16 @@ const McpTabContentInnerLoaded = ({
const track = useTrack()
const [selectedClient, setSelectedClient] = useState<McpClient | null>(null)
const handleCopy = (type?: 'url' | 'json' | 'command') => {
let connectionType: string
switch (type) {
case 'command':
connectionType = 'Command Line'
break
case 'json':
connectionType = 'JSON'
break
case 'url':
default:
connectionType = 'MCP URL'
break
}
track('connection_string_copied', {
connectionTab: 'MCP',
selectedItem: selectedClient?.label,
connectionType,
source: 'studio',
})
}
const handleCopy = useMemo(
() =>
createMcpCopyHandler({
selectedClient,
source: 'studio',
onTrack: (event) => track(event.action, event.properties, event.groups),
projectRef,
}),
[selectedClient, track, projectRef]
)
const handleInstall = () => {
if (selectedClient?.label) {
@@ -203,9 +203,8 @@ const CTANode = ({ projectRef }: { projectRef: string }) => {
<Badge variant="warning">Early Access</Badge>
</div>
<p className="text-foreground-light">
Replicate database changes to multiple destinations with ETL replication - no manual
exports, no lag. Limited rollout for external destinations has begun, read replicas
available now.
Replicate database changes to multiple destinations - no manual exports, no lag. Limited
rollout for external destinations has begun, read replicas available now.
</p>
<div className="flex items-center gap-x-2 mt-6">
<Button asChild type="secondary" iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}>
@@ -11,13 +11,13 @@ import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/Sto
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPrimaryKeysExists } from 'data/database/primary-keys-exists-query'
import { useCreateDestinationPipelineMutation } from 'data/etl/create-destination-pipeline-mutation'
import { useReplicationDestinationByIdQuery } from 'data/etl/destination-by-id-query'
import { useReplicationPipelineByIdQuery } from 'data/etl/pipeline-by-id-query'
import { useReplicationPublicationsQuery } from 'data/etl/publications-query'
import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { useUpdateDestinationPipelineMutation } from 'data/etl/update-destination-pipeline-mutation'
import { useCreateDestinationPipelineMutation } from 'data/replication/create-destination-pipeline-mutation'
import { useReplicationDestinationByIdQuery } from 'data/replication/destination-by-id-query'
import { useReplicationPipelineByIdQuery } from 'data/replication/pipeline-by-id-query'
import { useReplicationPublicationsQuery } from 'data/replication/publications-query'
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useUpdateDestinationPipelineMutation } from 'data/replication/update-destination-pipeline-mutation'
import { useIcebergNamespaceCreateMutation } from 'data/storage/iceberg-namespace-create-mutation'
import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -8,7 +8,7 @@ export const NoDestinationsAvailable = () => {
</div>
<h3 className="text-lg font-medium text-foreground mb-2">No destinations available</h3>
<p className="text-sm text-foreground-light max-w-lg">
ETL replication destinations are not currently enabled for this project. Contact support to
Replication destinations are not currently enabled for this project. Contact support to
enable real-time replication to external platforms.
</p>
</div>
@@ -4,7 +4,7 @@ import type { UseFormReturn } from 'react-hook-form'
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { useCheckPrimaryKeysExists } from 'data/database/primary-keys-exists-query'
import { useReplicationPublicationsQuery } from 'data/etl/publications-query'
import { useReplicationPublicationsQuery } from 'data/replication/publications-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { FormControl_Shadcn_, FormField_Shadcn_ } from 'ui'
import { Admonition } from 'ui-patterns'
@@ -6,12 +6,12 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { useDeleteDestinationPipelineMutation } from 'data/etl/delete-destination-pipeline-mutation'
import { useReplicationPipelineReplicationStatusQuery } from 'data/etl/pipeline-replication-status-query'
import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/etl/pipeline-version-query'
import { Pipeline } from 'data/etl/pipelines-query'
import { useStopPipelineMutation } from 'data/etl/stop-pipeline-mutation'
import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation'
import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query'
import { Pipeline } from 'data/replication/pipelines-query'
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
import {
PipelineStatusRequestStatus,
usePipelineRequestStatus,
@@ -179,7 +179,7 @@ export const DestinationRow = ({
<Table.td>
<div className="flex items-center justify-end gap-x-2">
<Button asChild type="default" className="relative">
<Link href={`/project/${projectRef}/database/etl/${pipeline?.id}`}>
<Link href={`/project/${projectRef}/database/replication/${pipeline?.id}`}>
<span className="inline-flex items-center gap-2">
<span>View status</span>
{hasTableErrors && (
@@ -7,11 +7,11 @@ import Table from 'components/to-be-cleaned/Table'
import { AlertError } from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { useReplicationDestinationsQuery } from 'data/etl/destinations-query'
import { replicationKeys } from 'data/etl/keys'
import { fetchReplicationPipelineVersion } from 'data/etl/pipeline-version-query'
import { useReplicationPipelinesQuery } from 'data/etl/pipelines-query'
import { useReplicationSourcesQuery } from 'data/etl/sources-query'
import { useReplicationDestinationsQuery } from 'data/replication/destinations-query'
import { replicationKeys } from 'data/replication/keys'
import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query'
import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query'
import { useReplicationSourcesQuery } from 'data/replication/sources-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { DOCS_URL } from 'lib/constants'
import { Button, cn, Input_Shadcn_ } from 'ui'
@@ -143,8 +143,8 @@ export const Destinations = () => {
<div className="flex flex-col gap-y-1">
<h3>Replicate data to external destinations in real-time</h3>
<p className="text-sm text-foreground-light">
{isPaidPlan ? 'Enable ETL replication' : 'Upgrade to the Pro plan'} to start
replicating your database changes to data warehouses and analytics platforms
{isPaidPlan ? 'Enable replication' : 'Upgrade to the Pro plan'} to start replicating
your database changes to data warehouses and analytics platforms
</p>
</div>
<div className="flex gap-x-2">
@@ -2,7 +2,7 @@ import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useCreateTenantSourceMutation } from 'data/etl/create-tenant-source-mutation'
import { useCreateTenantSourceMutation } from 'data/replication/create-tenant-source-mutation'
import {
Button,
Dialog,
@@ -23,11 +23,11 @@ export const EnableReplicationModal = () => {
const { mutate: createTenantSource, isPending: creatingTenantSource } =
useCreateTenantSourceMutation({
onSuccess: () => {
toast.success('ETL replication has been successfully enabled!')
toast.success('Replication has been successfully enabled!')
setOpen(false)
},
onError: (error) => {
toast.error(`Failed to enable ETL replication: ${error.message}`)
toast.error(`Failed to enable replication: ${error.message}`)
},
})
@@ -40,26 +40,26 @@ export const EnableReplicationModal = () => {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button type="primary" className="w-min">
Enable ETL replication
Enable replication
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Enable ETL Replication</DialogTitle>
<DialogTitle>Enable Replication</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-2 !p-0">
<Admonition
type="warning"
className="rounded-none border-0 mb-0"
title="ETL Replication is currently in Alpha"
title="Replication is currently in Alpha"
>
<p className="text-sm !leading-normal">
This feature is in active development and may change as we gather feedback.
Availability and behavior can evolve while in Alpha.
</p>
<p className="text-sm !leading-normal">
Pricing has not been finalized yet. You can enable ETL replication now; we'll announce
Pricing has not been finalized yet. You can enable replication now; we'll announce
pricing later and notify you before any charges apply.
</p>
</Admonition>
@@ -69,7 +69,7 @@ export const EnableReplicationModal = () => {
Cancel
</Button>
<Button type="primary" loading={creatingTenantSource} onClick={onEnableReplication}>
Enable ETL replication
Enable replication
</Button>
</DialogFooter>
</DialogContent>
@@ -5,8 +5,8 @@ import { toast } from 'sonner'
import { z } from 'zod'
import { useParams } from 'common'
import { useCreatePublicationMutation } from 'data/etl/publication-create-mutation'
import { useReplicationTablesQuery } from 'data/etl/tables-query'
import { useCreatePublicationMutation } from 'data/replication/publication-create-mutation'
import { useReplicationTablesQuery } from 'data/replication/tables-query'
import {
Button,
cn,
@@ -1,7 +1,7 @@
import {
ReplicationPipelineStatus,
ReplicationPipelineStatusData,
} from 'data/etl/pipeline-status-query'
} from 'data/replication/pipeline-status-query'
import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status'
import { PipelineStatusName } from './Replication.constants'
@@ -3,7 +3,7 @@ import { AlertTriangle, Loader2 } from 'lucide-react'
import { useParams } from 'common'
import { DotPing } from 'components/ui/DotPing'
import { InlineLink } from 'components/ui/InlineLink'
import { ReplicationPipelineStatusData } from 'data/etl/pipeline-status-query'
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status'
import type { ResponseError } from 'types'
import { cn, Tooltip, TooltipContent, TooltipTrigger, WarningIcon } from 'ui'
@@ -134,10 +134,10 @@ export const PipelineStatus = ({
const statusConfig = getStatusConfig()
const pipelineLogsUrl = pipelineId
? `/project/${ref}/logs/etl-replication-logs?f=${encodeURIComponent(
? `/project/${ref}/logs/replication-logs?f=${encodeURIComponent(
JSON.stringify({ pipeline_id: pipelineId })
)}`
: `/project/${ref}/logs/etl-replication-logs`
: `/project/${ref}/logs/replication-logs`
return (
<>
@@ -21,13 +21,13 @@ import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useReplicationPipelineByIdQuery } from 'data/etl/pipeline-by-id-query'
import { useReplicationPipelineReplicationStatusQuery } from 'data/etl/pipeline-replication-status-query'
import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/etl/pipeline-version-query'
import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { useStopPipelineMutation } from 'data/etl/stop-pipeline-mutation'
import { useReplicationPipelineByIdQuery } from 'data/replication/pipeline-by-id-query'
import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query'
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
import {
PipelineStatusRequestStatus,
usePipelineRequestStatus,
@@ -134,7 +134,7 @@ export const ReplicationPipelineStatus = () => {
? `${Math.round(STATUS_REFRESH_FREQUENCY_MS / 1000)}s`
: `${STATUS_REFRESH_FREQUENCY_MS}ms`
const logsUrl = `/project/${projectRef}/logs/etl-replication-logs${
const logsUrl = `/project/${projectRef}/logs/replication-logs${
pipelineId ? `?f=${encodeURIComponent(JSON.stringify({ pipeline_id: pipelineId }))}` : ''
}`
@@ -188,7 +188,7 @@ export const ReplicationPipelineStatus = () => {
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-3">
<Button asChild type="outline" icon={<ChevronLeft />} style={{ padding: '5px' }}>
<Link href={`/project/${projectRef}/database/etl`} />
<Link href={`/project/${projectRef}/database/replication`} />
</Button>
<div className="flex items-center gap-x-3">
<h3 className="text-xl font-semibold">{destinationName || 'Pipeline'}</h3>
@@ -1,7 +1,7 @@
import dayjs from 'dayjs'
import { Activity, Clock, HelpCircle, Loader2, XCircle } from 'lucide-react'
import { ReplicationPipelineStatusData } from 'data/etl/pipeline-status-query'
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
import { formatBytes } from 'lib/helpers'
import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status'
import { Badge } from 'ui'
@@ -3,7 +3,7 @@ import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useTableReset } from 'data/etl/use-table-reset'
import { useTableReset } from 'data/replication/use-table-reset'
import {
AlertDialog,
AlertDialogAction,
@@ -3,8 +3,8 @@ import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper'
import { RollbackType, useRollbackTableMutation } from 'data/etl/rollback-table-mutation'
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
import { RollbackType, useRollbackTableMutation } from 'data/replication/rollback-table-mutation'
import {
Button,
DropdownMenu,
@@ -3,11 +3,11 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { ReplicationPipelineStatusData } from 'data/etl/pipeline-status-query'
import { Pipeline } from 'data/etl/pipelines-query'
import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { useStopPipelineMutation } from 'data/etl/stop-pipeline-mutation'
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
import { Pipeline } from 'data/replication/pipelines-query'
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
import {
PipelineStatusRequestStatus,
usePipelineRequestStatus,
@@ -1,11 +1,11 @@
import { toast } from 'sonner'
import { useParams } from 'common'
import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/etl/pipeline-version-query'
import { Pipeline } from 'data/etl/pipelines-query'
import { useRestartPipelineHelper } from 'data/etl/restart-pipeline-helper'
import { useUpdatePipelineVersionMutation } from 'data/etl/update-pipeline-version-mutation'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query'
import { Pipeline } from 'data/replication/pipelines-query'
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
import { useUpdatePipelineVersionMutation } from 'data/replication/update-pipeline-version-mutation'
import { ChevronDown } from 'lucide-react'
import {
PipelineStatusRequestStatus,
@@ -101,16 +101,6 @@ export const RestoreToNewProject = () => {
const isRestoring = previousClones?.some((c) => c.status === 'IN_PROGRESS')
const restoringClone = previousClones?.find((c) => c.status === 'IN_PROGRESS')
if (organization?.managed_by === 'vercel-marketplace') {
return (
<Admonition
type="default"
title="Restore to new project is not available for Vercel Marketplace organizations"
description="Restoring project backups to a new project created via Vercel Marketplace is not supported yet."
/>
)
}
if (isFreePlan) {
return (
<UpgradeToPro
@@ -0,0 +1,88 @@
import { ArrowDownNarrowWide, ArrowDownWideNarrow } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from 'ui'
export const EDGE_FUNCTIONS_SORT_VALUES = [
'name:asc',
'name:desc',
'created_at:asc',
'created_at:desc',
'updated_at:asc',
'updated_at:desc',
] as const
export type EdgeFunctionsSort = (typeof EDGE_FUNCTIONS_SORT_VALUES)[number]
export type EdgeFunctionsSortColumn = EdgeFunctionsSort extends `${infer Column}:${string}`
? Column
: unknown
export type EdgeFunctionsSortOrder = EdgeFunctionsSort extends `${string}:${infer Order}`
? Order
: unknown
interface EdgeFunctionsSortDropdownProps {
value: EdgeFunctionsSort
onChange: (value: EdgeFunctionsSort) => void
}
function getSortLabel(value: EdgeFunctionsSort) {
const [sortCol] = value.split(':')
return sortCol.replace('_', ' ')
}
export const EdgeFunctionsSortDropdown = ({ value, onChange }: EdgeFunctionsSortDropdownProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={
value.includes('desc') ? (
<ArrowDownWideNarrow size={14} />
) : (
<ArrowDownNarrowWide size={14} />
)
}
>
Sorted by {getSortLabel(value)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuRadioGroup
value={value}
onValueChange={(val) => onChange(val as EdgeFunctionsSort)}
>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Sort by name</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value="name:asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name:desc">Descending</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Sort by created at</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value="created_at:asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="created_at:desc">Descending</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Sort by updated at</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value="updated_at:asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="updated_at:desc">Descending</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -3,8 +3,8 @@ import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { PopoverSeparator } from '@ui/components/shadcn/ui/popover'
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { useBranchesQuery } from 'data/branches/branches-query'
import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'
import {
@@ -335,16 +335,27 @@ export const ServiceStatus = () => {
</div>
</Link>
))}
{allServicesOperational ? null : (
<>
<PopoverSeparator />
<div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
<div className="mt-0.5">
<InfoIcon />
</div>
Recently restored projects can take up to 5 minutes to become fully operational.
{!allServicesOperational && (
<div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
<div className="mt-0.5">
<InfoIcon />
</div>
</>
<div className="flex flex-col gap-y-1">
<p>
{isProjectNew ? 'New' : 'Recently restored'} projects can take up to{' '}
{SERVICE_STATUS_THRESHOLD} minutes to become fully operational.
</p>
<p>
If services stay unhealthy, refer to our{' '}
<InlineLink
href={`${DOCS_URL}/guides/troubleshooting/project-status-reports-unhealthy-services`}
>
docs
</InlineLink>{' '}
for more information.
</p>
</div>
</div>
)}
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
@@ -1,8 +1,9 @@
import dayjs from 'dayjs'
import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { PopoverSeparator } from '@ui/components/shadcn/ui/popover'
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import { SingleStat } from 'components/ui/SingleStat'
import { useBranchesQuery } from 'data/branches/branches-query'
import { useEdgeFunctionServiceStatusQuery } from 'data/service-status/edge-functions-status-query'
@@ -15,6 +16,8 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { InfoIcon, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui'
const SERVICE_STATUS_THRESHOLD = 5 // minutes
/**
* [Joshen] JFYI before we go live with this, we need to revisit the migrations section
* as I don't think it should live in the ServiceStatus component since its not indicative
@@ -248,6 +251,7 @@ export const ServiceStatus = () => {
// Check if project or branch is in a startup state
const isProjectNew =
dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < SERVICE_STATUS_THRESHOLD ||
project?.status === 'COMING_UP' ||
(isBranch &&
(currentBranch?.status === 'CREATING_PROJECT' ||
@@ -301,7 +305,7 @@ export const ServiceStatus = () => {
value={<span>{overallStatusLabel}</span>}
/>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ portal className="p-0 w-56" side="bottom" align="start">
<PopoverContent_Shadcn_ portal className="p-0 w-60" side="bottom" align="start">
{services.map((service) => (
<Link
href={`/project/${ref}${service.logsUrl}`}
@@ -333,16 +337,27 @@ export const ServiceStatus = () => {
</div>
</Link>
))}
{allServicesOperational ? null : (
<>
<PopoverSeparator />
<div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
<div className="mt-0.5">
<InfoIcon />
</div>
Recently restored projects can take up to 5 minutes to become fully operational.
{!allServicesOperational && (
<div className="flex gap-2 text-xs text-foreground-light px-3 py-2">
<div className="mt-0.5">
<InfoIcon />
</div>
</>
<div className="flex flex-col gap-y-1">
<p>
{isProjectNew ? 'New' : 'Recently restored'} projects can take up to{' '}
{SERVICE_STATUS_THRESHOLD} minutes to become fully operational.
</p>
<p>
If services stay unhealthy, refer to our{' '}
<InlineLink
href={`${DOCS_URL}/guides/troubleshooting/project-status-reports-unhealthy-services`}
>
docs
</InlineLink>{' '}
for more information.
</p>
</div>
</div>
)}
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
@@ -1,34 +0,0 @@
import InformationBox from 'components/ui/InformationBox'
import { AlertCircle } from 'lucide-react'
interface NotOrganizationOwnerWarningProps {
slug?: string
}
// [Joshen] This can just use NoPermission component i think
export const NotOrganizationOwnerWarning = ({ slug }: NotOrganizationOwnerWarningProps) => {
return (
<div className="mt-4">
<InformationBox
icon={<AlertCircle size="20" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="You do not have permission to create a project"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
{slug ? (
<>
Contact the owner or administrator to create a new project in the{' '}
<code>{slug}</code> organization.
</>
) : (
<>Contact the owner or administrator to create a new project.</>
)}
</p>
</div>
}
/>
</div>
)
}
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import { UseFormReturn } from 'react-hook-form'
import { useParams } from 'common'
import { NoPermission } from 'components/ui/NoPermission'
import Panel from 'components/ui/Panel'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -20,7 +21,6 @@ import {
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { OrgNotFound } from '../Organization/OrgNotFound'
import { NotOrganizationOwnerWarning } from './NotOrganizationOwnerWarning'
import { CreateProjectForm } from './ProjectCreation.schema'
interface OrganizationSelectorProps {
@@ -39,49 +39,47 @@ export const OrganizationSelector = ({ form }: OrganizationSelectorProps) => {
return (
<Panel.Content className="space-y-4">
{isAdmin && !isInvalidSlug && (
<FormField_Shadcn_
control={form.control}
name="organization"
render={({ field }) => (
<FormItemLayout label="Organization" layout="horizontal">
{(organizations?.length ?? 0) > 0 && (
<Select_Shadcn_
onValueChange={(slug) => {
field.onChange(slug)
router.push(`/new/${slug}`)
}}
value={field.value}
defaultValue={field.value}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{organizations?.map((x) => (
<SelectItem_Shadcn_
key={x.id}
value={x.slug}
className="flex justify-between"
>
<span className="mr-2">{x.name}</span>
<Badge>{x.plan.name}</Badge>
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)}
</FormItemLayout>
)}
/>
)}
<FormField_Shadcn_
control={form.control}
name="organization"
render={({ field }) => (
<FormItemLayout label="Organization" layout="horizontal">
{(organizations?.length ?? 0) > 0 && (
<Select_Shadcn_
onValueChange={(slug) => {
field.onChange(slug)
router.push(`/new/${slug}`)
}}
value={field.value}
defaultValue={field.value}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{organizations?.map((x) => (
<SelectItem_Shadcn_
key={x.id}
value={x.slug}
className="flex justify-between"
>
<span className="mr-2">{x.name}</span>
<Badge>{x.plan.name}</Badge>
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)}
</FormItemLayout>
)}
/>
{isOrganizationsSuccess && !isAdmin && !orgNotFound && (
<NotOrganizationOwnerWarning slug={slug} />
<NoPermission resourceText="create a project" />
)}
{orgNotFound && <OrgNotFound slug={slug} />}
</Panel.Content>
@@ -1,6 +1,7 @@
import { X } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useParams } from 'common'
import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
@@ -9,8 +10,8 @@ import { DEFAULT_CHART_CONFIG, QueryBlock } from 'components/ui/QueryBlock/Query
import { AnalyticsInterval } from 'data/analytics/constants'
import { useContentIdQuery } from 'data/content/content-id-query'
import { usePrimaryDatabase } from 'data/read-replicas/replicas-query'
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useChangedSync } from 'hooks/misc/useChanged'
import { executeSql } from 'data/sql/execute-sql-query'
import { sqlKeys } from 'data/sql/keys'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import type { Dashboards, SqlSnippets } from 'types'
import { DEPRECATED_REPORTS } from '../Reports.constants'
@@ -47,9 +48,15 @@ export const ReportBlock = ({
const { ref: projectRef } = useParams()
const state = useDatabaseSelectorStateSnapshot()
const [isWriteQuery, setIsWriteQuery] = useState(false)
const isSnippet = item.attribute.startsWith('snippet_')
const { data, error, isLoading, isSuccess } = useContentIdQuery(
const {
data,
error: contentError,
isLoading: isLoadingContent,
} = useContentIdQuery(
{ projectRef, id: item.id },
{
enabled: isSnippet && !!item.id,
@@ -66,81 +73,65 @@ export const ReportBlock = ({
const sql = isSnippet ? (data?.content as SqlSnippets.Content)?.sql : undefined
const chartConfig = { ...DEFAULT_CHART_CONFIG, ...(item.chartConfig ?? {}) }
const isDeprecatedChart = DEPRECATED_REPORTS.includes(item.attribute)
const snippetMissing = error?.message.includes('Content not found')
const snippetMissing = contentError?.message.includes('Content not found')
const { database: primaryDatabase } = usePrimaryDatabase({ projectRef })
const readOnlyConnectionString = primaryDatabase?.connection_string_read_only
const postgresConnectionString = primaryDatabase?.connectionString
const [rows, setRows] = useState<any[] | undefined>(undefined)
const [isWriteQuery, setIsWriteQuery] = useState(false)
const {
mutate: executeSql,
data: queryResult,
error: executeSqlError,
isLoading: executeSqlLoading,
} = useExecuteSqlMutation({
onError: () => {
// Silence the error toast because the error will be displayed inline
},
})
refetch,
} = useQuery({
queryKey: sqlKeys.query(projectRef, [
item.id,
sql,
readOnlyConnectionString,
postgresConnectionString,
]),
queryFn: async () => {
if (!projectRef || !sql) return null
const runQuery = useCallback(
(queryType: 'select' | 'mutation' = 'select', sqlToRun?: string) => {
if (!projectRef || !sqlToRun) return false
const connectionString =
queryType === 'mutation'
? postgresConnectionString
: readOnlyConnectionString ?? postgresConnectionString
const connectionString = readOnlyConnectionString ?? postgresConnectionString
if (!connectionString) {
toast.error('Unable to establish a database connection for this project.')
return false
return null
}
if (queryType === 'mutation') {
setIsWriteQuery(true)
}
executeSql(
{ projectRef, connectionString, sql: sqlToRun },
{
onSuccess: (data) => {
setRows(data.result)
setIsWriteQuery(queryType === 'mutation')
},
onError: (mutationError) => {
const lowerMessage = mutationError.message.toLowerCase()
const isReadOnlyError =
lowerMessage.includes('read-only transaction') ||
lowerMessage.includes('permission denied') ||
lowerMessage.includes('must be owner')
if (queryType === 'select' && isReadOnlyError) {
setIsWriteQuery(true)
}
},
}
)
return true
return executeSql({
projectRef,
connectionString,
sql,
})
},
[projectRef, readOnlyConnectionString, postgresConnectionString, executeSql]
)
enabled: !isLoadingContent && contentError == null,
refetchOnWindowFocus: false,
})
const rows = queryResult?.result
useEffect(() => {
if (!isSuccess) return
if (!isSnippet) return
const fetchedSql = (data?.content as SqlSnippets.Content | undefined)?.sql
if (fetchedSql) {
runQuery('select', fetchedSql)
}
}, [data, isSuccess, runQuery, isSnippet])
if (executeSqlError) {
const errorMessage = String(executeSqlError).toLowerCase()
const isReadOnlyError =
errorMessage.includes('read-only transaction') ||
errorMessage.includes('permission denied') ||
errorMessage.includes('must be owner')
const sqlHasChanged = useChangedSync(sql)
const isRefreshingChanged = useChangedSync(isRefreshing)
if (sqlHasChanged || (isRefreshingChanged && isRefreshing)) {
runQuery('select', sql)
}
if (isReadOnlyError) {
setIsWriteQuery(true)
}
}
}, [executeSqlError])
useEffect(() => {
if (isRefreshing) {
refetch()
}
}, [isRefreshing, refetch])
return (
<>
@@ -153,11 +144,17 @@ export const ReportBlock = ({
sql={sql}
results={rows}
initialHideSql={true}
errorText={snippetMissing ? 'SQL snippet not found' : executeSqlError?.message}
errorText={
snippetMissing
? 'SQL snippet not found'
: executeSqlError
? String(executeSqlError)
: undefined
}
isExecuting={executeSqlLoading}
isWriteQuery={isWriteQuery}
actions={
!isLoading && (
!isLoadingContent && (
<ButtonTooltip
type="text"
icon={<X />}
@@ -168,11 +165,11 @@ export const ReportBlock = ({
)
}
onExecute={(queryType) => {
runQuery(queryType, sql)
refetch()
}}
onUpdateChartConfig={onUpdateChart}
onRemoveChart={() => onRemoveChart({ metric: { key: item.attribute } })}
disabled={isLoading || snippetMissing || !sql}
disabled={isLoadingContent || snippetMissing || !sql}
/>
) : isDeprecatedChart ? (
<DeprecatedChartBlock
@@ -1,6 +1,5 @@
import { AlertCircle, HelpCircle, RefreshCw } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
@@ -9,9 +8,9 @@ import Panel from 'components/ui/Panel'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useCustomDomainReverifyMutation } from 'data/custom-domains/custom-domains-reverify-mutation'
import { useInterval } from 'hooks/misc/useInterval'
import { useCustomDomainReverifyQuery } from 'data/custom-domains/custom-domains-reverify-query'
import { DOCS_URL } from 'lib/constants'
import { useEffect } from 'react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -24,7 +23,6 @@ import { DNSTableHeaders } from './DNSTableHeaders'
const CustomDomainVerify = () => {
const { ref: projectRef } = useParams()
const [isNotVerifiedYet, setIsNotVerifiedYet] = useState(false)
const { data: settings } = useProjectSettingsV2Query({ projectRef })
@@ -33,13 +31,6 @@ const CustomDomainVerify = () => {
const isSSLCertificateDeploying =
customDomain?.ssl.status !== undefined && customDomain.ssl.txt_name === undefined
const { mutate: reverifyCustomDomain, isPending: isReverifyLoading } =
useCustomDomainReverifyMutation({
onSuccess: (res) => {
if (res.status === '2_initiated') setIsNotVerifiedYet(true)
},
})
const { mutate: deleteCustomDomain, isPending: isDeleting } = useCustomDomainDeleteMutation({
onSuccess: () => {
toast.success(
@@ -48,6 +39,28 @@ const CustomDomainVerify = () => {
},
})
const {
data: reverifyData,
refetch: refetchReverify,
isFetching: isReverifyLoading,
isError: isReverifyError,
error: reverifyError,
} = useCustomDomainReverifyQuery(
{ projectRef },
{
// Poll every 10 seconds if the SSL certificate is being deployed
refetchInterval: isSSLCertificateDeploying && !isDeleting ? 10000 : false,
}
)
useEffect(() => {
if (isReverifyError) {
toast.error(reverifyError?.message)
}
}, [isReverifyError, reverifyError])
const isNotVerifiedYet = reverifyData?.status === '2_initiated'
const hasCAAErrors = customDomain?.ssl.validation_errors?.reduce(
(acc, error) => acc || error.message.includes('caa_error'),
false
@@ -56,15 +69,9 @@ const CustomDomainVerify = () => {
const onReverifyCustomDomain = () => {
if (!projectRef) return console.error('Project ref is required')
reverifyCustomDomain({ projectRef })
refetchReverify()
}
useInterval(
onReverifyCustomDomain,
// Poll every 5 seconds if the SSL certificate is being deployed
isSSLCertificateDeploying && !isDeleting ? 5000 : false
)
const onCancelCustomDomain = async () => {
if (!projectRef) return console.error('Project ref is required')
deleteCustomDomain({ projectRef })
@@ -423,7 +423,7 @@ export const LOGS_SOURCE_DESCRIPTION = {
[LogsTableName.PGBOUNCER]: 'Dedicated connection pooler for PostgreSQL',
[LogsTableName.PG_UPGRADE]: 'Logs generated by the Postgres version upgrade process',
[LogsTableName.PG_CRON]: 'Postgres logs from pg_cron cron jobs',
[LogsTableName.ETL]: 'Logs from the ETL replication process',
[LogsTableName.ETL]: 'Logs from the replication process',
}
export const FILTER_OPTIONS: FilterTableSet = {
@@ -44,7 +44,7 @@ export const AlphaNotice = ({ type }: { type: 'analytics' | 'vector' }) => {
type === 'analytics'
? 'https://github.com/orgs/supabase/discussions/40116'
: // [Joshen] To update with Vector specific GH discussion
'https://github.com/orgs/supabase/discussions/40116'
'https://github.com/orgs/supabase/discussions/40815'
}
>
Share feedback
@@ -8,18 +8,18 @@ import z from 'zod'
import { useFlag, useParams } from 'common'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/ETL/useIsETLPrivateAlpha'
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
import { convertKVStringArrayToJson } from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCreateDestinationPipelineMutation } from 'data/etl/create-destination-pipeline-mutation'
import { useCreateTenantSourceMutation } from 'data/etl/create-tenant-source-mutation'
import { useCreatePublicationMutation } from 'data/etl/publication-create-mutation'
import { useUpdatePublicationMutation } from 'data/etl/publication-update-mutation'
import { useReplicationSourcesQuery } from 'data/etl/sources-query'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { useReplicationTablesQuery } from 'data/etl/tables-query'
import { useCreateDestinationPipelineMutation } from 'data/replication/create-destination-pipeline-mutation'
import { useCreateTenantSourceMutation } from 'data/replication/create-tenant-source-mutation'
import { useCreatePublicationMutation } from 'data/replication/publication-create-mutation'
import { useUpdatePublicationMutation } from 'data/replication/publication-update-mutation'
import { useReplicationSourcesQuery } from 'data/replication/sources-query'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useReplicationTablesQuery } from 'data/replication/tables-query'
import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import {
@@ -255,7 +255,7 @@ export const CreateTableInstructions = ({
<CardFooter className="bg">
<p className="text-xs text-foreground-light">
Connecting to bucket with other Iceberg clients? Read more in our{' '}
<InlineLink href={`${DOCS_URL}/guides/storage/analytics/connecting-to-analytics-bucket`}>
<InlineLink href={`${DOCS_URL}/guides/storage/analytics/examples/pyiceberg`}>
documentation
</InlineLink>
.
@@ -5,10 +5,12 @@ import { toast } from 'sonner'
import z from 'zod'
import { useParams } from 'common'
import { DocsButton } from 'components/ui/DocsButton'
import { useSchemaCreateMutation } from 'data/database/schema-create-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useFDWImportForeignSchemaMutation } from 'data/fdw/fdw-import-foreign-schema-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import {
Button,
Dialog,
@@ -113,13 +115,16 @@ export const InitializeForeignSchemaDialog = ({ namespace }: { namespace: string
)}
/>
</DialogSection>
<DialogFooter>
<Button type="default" disabled={isCreating} onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button htmlType="submit" type="primary" loading={isCreating}>
Create schema
</Button>
<DialogFooter className="!justify-between">
<DocsButton href={`${DOCS_URL}/guides/storage/analytics/query-with-postgres`} />
<div className="flex items-center gap-x-2">
<Button type="default" disabled={isCreating} onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button htmlType="submit" type="primary" loading={isCreating}>
Create schema
</Button>
</div>
</DialogFooter>
</form>
</Form_Shadcn_>
@@ -1,6 +1,6 @@
import { snakeCase } from 'lodash'
import { ReplicationPublication } from 'data/etl/publications-query'
import { ReplicationPublication } from 'data/replication/publications-query'
export const inferPostgresTableFromNamespaceTable = ({
publication,
@@ -12,10 +12,10 @@ import {
import { getDecryptedParameters } from 'components/interfaces/Storage/ImportForeignSchemaDialog.utils'
import { DotPing } from 'components/ui/DotPing'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query'
import { useUpdatePublicationMutation } from 'data/etl/publication-update-mutation'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { useReplicationTablesQuery } from 'data/etl/tables-query'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useUpdatePublicationMutation } from 'data/replication/publication-update-mutation'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useReplicationTablesQuery } from 'data/replication/tables-query'
import { useFDWDropForeignTableMutation } from 'data/fdw/fdw-drop-foreign-table-mutation'
import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import { useIcebergNamespaceTableDeleteMutation } from 'data/storage/iceberg-namespace-table-delete-mutation'
@@ -342,7 +342,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
{!!inferredPostgresTable && (
<DropdownMenuItem asChild className="flex items-center gap-x-2">
<Link
href={`/project/${projectRef}/database/etl/${pipeline?.id}?search=${inferredPostgresTable.schema}.${inferredPostgresTable.name}`}
href={`/project/${projectRef}/database/replication/${pipeline?.id}?search=${inferredPostgresTable.schema}.${inferredPostgresTable.name}`}
>
<Eye size={12} className="text-foreground-lighter" />
<p>View replication</p>
@@ -5,9 +5,11 @@ import { toast } from 'sonner'
import z from 'zod'
import { useParams } from 'common'
import { DocsButton } from 'components/ui/DocsButton'
import { InlineLinkClassName } from 'components/ui/InlineLink'
import { useFDWImportForeignSchemaMutation } from 'data/fdw/fdw-import-foreign-schema-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import {
Button,
Dialog,
@@ -153,13 +155,16 @@ export const UpdateForeignSchemaDialog = ({
</p>
)}
</DialogSection>
<DialogFooter>
<Button type="default" disabled={isUpdating} onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button htmlType="submit" type="primary" loading={isUpdating}>
Update schema
</Button>
<DialogFooter className="!justify-between">
<DocsButton href={`${DOCS_URL}/guides/storage/analytics/query-with-postgres`} />
<div className="flex items-center gap-x-2">
<Button type="default" disabled={isUpdating} onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button htmlType="submit" type="primary" loading={isUpdating}>
Update schema
</Button>
</div>
</DialogFooter>
</form>
</Form_Shadcn_>
@@ -23,8 +23,8 @@ import {
DatabaseExtension,
useDatabaseExtensionsQuery,
} from 'data/database-extensions/database-extensions-query'
import { useReplicationPipelineStatusQuery } from 'data/etl/pipeline-status-query'
import { useStartPipelineMutation } from 'data/etl/start-pipeline-mutation'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
import { useIcebergNamespacesQuery } from 'data/storage/iceberg-namespaces-query'
import { useIcebergWrapperCreateMutation } from 'data/storage/iceberg-wrapper-create-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -270,7 +270,7 @@ export const AnalyticBucketDetails = () => {
<div className="flex items-center gap-x-2">
<Button asChild type="default">
<Link
href={`/project/${projectRef}/database/etl/${pipeline.replicator_id}`}
href={`/project/${projectRef}/database/replication/${pipeline.replicator_id}`}
>
View replication
</Link>
@@ -1,11 +1,11 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDeleteDestinationPipelineMutation } from 'data/etl/delete-destination-pipeline-mutation'
import { useReplicationDestinationsQuery } from 'data/etl/destinations-query'
import { useReplicationPipelinesQuery } from 'data/etl/pipelines-query'
import { useDeletePublicationMutation } from 'data/etl/publication-delete-mutation'
import { useReplicationPublicationsQuery } from 'data/etl/publications-query'
import { useReplicationSourcesQuery } from 'data/etl/sources-query'
import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation'
import { useReplicationDestinationsQuery } from 'data/replication/destinations-query'
import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query'
import { useDeletePublicationMutation } from 'data/replication/publication-delete-mutation'
import { useReplicationPublicationsQuery } from 'data/replication/publications-query'
import { useReplicationSourcesQuery } from 'data/replication/sources-query'
import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation'
import { useS3AccessKeyDeleteMutation } from 'data/storage/s3-access-key-delete-mutation'
import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query'
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff. Show more