Merge branch 'master' into self-hosted/docs-oauth-oidc

This commit is contained in:
Chris Chinchilla authored and GitHub committed 2026-03-03 15:04:08 +01:00
commit c7a9fc0d53
108 files changed
+3636 -1103

No files matched your search

@@ -1,5 +1,5 @@
import { Mdx } from '@/components/mdx-components'
import { DocsPager } from '@/components/pager'
import { DocsPager, getBreadcrumbSegments } from '@/components/pager'
import { SourcePanel } from '@/components/source-panel'
import { DashboardTableOfContents } from '@/components/toc'
import { siteConfig } from '@/config/site'
@@ -12,6 +12,7 @@ import '@/styles/mdx.css'
import { allDocs } from 'contentlayer/generated'
import { ChevronRight } from 'lucide-react'
import type { Metadata } from 'next'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea, Separator } from 'ui'
@@ -83,15 +84,39 @@ export default async function DocPage(props: DocPageProps) {
}
const toc = await getTableOfContents(doc.body.raw)
const breadcrumbSegments = getBreadcrumbSegments(doc)
return (
<div className="relative xl:grid xl:grid-cols-[1fr_160px] gap-4 px-6 py-6 lg:py-8">
<div className="mx-auto w-full min-w-0 max-w-4xl flex-1">
<div className="mb-4 flex items-center space-x-1 text-sm text-foreground-muted">
<div className="overflow-hidden text-ellipsis whitespace-nowrap">Docs</div>
<ChevronRight className="h-4 w-4 text-foreground-muted" />
<div className="text-foreground-lighter">{doc.title}</div>
</div>
<nav
className="mb-4 flex items-center space-x-1 text-sm text-foreground-lighter"
aria-label="Breadcrumb"
>
{breadcrumbSegments.map((segment, index) => (
<span key={segment.title} className="flex items-center gap-1">
{index > 0 && <ChevronRight className="h-4 w-4 shrink-0 text-foreground-muted" />}
{segment.href ? (
<Link
href={segment.href}
className="overflow-hidden text-ellipsis whitespace-nowrap hover:text-foreground transition-colors"
>
{segment.title}
</Link>
) : (
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-foreground-lighter">
{segment.title}
</span>
)}
</span>
))}
{breadcrumbSegments.length > 0 && (
<ChevronRight className="h-4 w-4 shrink-0 text-foreground-muted/50" />
)}
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-foreground-muted">
{doc.title}
</span>
</nav>
<div className="space-y-2 mb-5">
<h1 className="scroll-m-20 text-4xl tracking-tight">{doc.title}</h1>
{doc.description && (
+24
View File
@@ -71,3 +71,27 @@ export function flatten(links: NavItemWithChildren[]): NavItem[] {
}, [])
.filter((link) => !link?.disabled)
}
export interface BreadcrumbSegment {
title: string
href?: string
}
export function getBreadcrumbSegments(doc: Doc): BreadcrumbSegment[] {
const segments: BreadcrumbSegment[] = [{ title: 'Docs', href: '/docs' }]
for (const section of docsConfig.sidebarNav) {
const flatItems = flatten(section.items ?? [])
const inSection = flatItems.some((item) => item.href === doc.slug)
if (!inSection || !section.title) continue
const sectionLanding = flatItems.find((item) => item.priority) ?? flatItems[0]
const isOnLanding = sectionLanding?.href === doc.slug
if (sectionLanding?.href && !isOnLanding) {
segments.push({ title: section.title, href: sectionLanding.href })
}
break
}
return segments
}
@@ -1050,6 +1050,10 @@ export const database: NavMenuConstant = {
name: 'Partitioning your tables',
url: '/guides/database/partitions' as `/${string}`,
},
{
name: 'Migrating to pg_partman',
url: '/guides/database/migrating-to-pg-partman' as `/${string}`,
},
{
name: 'Managing connections',
url: '/guides/database/connection-management' as `/${string}`,
@@ -1250,6 +1254,10 @@ export const database: NavMenuConstant = {
name: 'pg_net: Async Networking',
url: '/guides/database/extensions/pg_net' as `/${string}`,
},
{
name: 'pg_partman: Partition management',
url: '/guides/database/extensions/pg_partman' as `/${string}`,
},
{
name: 'pg_plan_filter: Restrict Total Cost',
url: '/guides/database/extensions/pg_plan_filter' as `/${string}`,
+1 -1
View File
@@ -69,7 +69,7 @@ Otherwise sessions are progressively deleted from the database 24 hours after th
### What are recommended values for access token (JWT) expiration?
Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/settings/jwt) in the Advanced Settings section.
Most applications should use the default expiration time of 1 hour. This can be customized in your [project's settings](/dashboard/project/_/settings/jwt/legacy) in the JWT Keys > Legacy JWT Secret section.
Setting a value over 1 hour is generally discouraged for security reasons, but it may make sense in certain situations.
@@ -0,0 +1,95 @@
---
id: 'pg_partman'
title: 'pg_partman: partition management'
description: 'Automated partition management'
---
[`pg_partman`](https://github.com/pgpartman/pg_partman) is a Postgres extension that automates the creation and maintenance of partitions for tables using Postgres native partitioning.
## Enable the extension
To enable `pg_partman`, create a dedicated schema for it and enable the extension there.
{/* prettier-ignore */}
```sql
create schema if not exists partman;
create extension if not exists pg_partman with schema partman;
```
## Create a partitioned table
`pg_partman` requires your parent table to already be declared as a partitioned table.
{/* prettier-ignore */}
```sql
create table public.messages (
id bigint generated by default as identity,
sent_at timestamptz not null,
sender_id uuid,
recipient_id uuid,
body text,
primary key (sent_at, id)
)
partition by range (sent_at);
```
## Set up partitioning
You configure the parent table using `partman.create_parent()`. The function takes an `ACCESS EXCLUSIVE` lock briefly while it creates the initial partitions.
### Time-based partitions
{/* prettier-ignore */}
```sql
select partman.create_parent(
p_parent_table := 'public.messages',
p_control := 'sent_at',
p_type := 'range',
p_interval := '7 days',
p_premake := 7,
p_start_partition := '2025-01-01 00:00:00'
);
```
### Integer-based partitions
{/* prettier-ignore */}
```sql
create table public.events (
id bigint generated by default as identity,
inserted_at timestamptz not null default now(),
payload jsonb,
primary key (id)
)
partition by range (id);
select partman.create_parent(
p_parent_table := 'public.events',
p_control := 'id',
p_type := 'range',
p_interval := '100000'
);
```
## Running maintenance
It’s important to call `pg_partman` maintenance regularly so future partitions are pre-created and retention policies are applied.
{/* prettier-ignore */}
```sql
call partman.run_maintenance_proc();
```
To automate this, schedule it using `pg_cron`.
{/* prettier-ignore */}
```sql
create extension if not exists pg_cron;
select
cron.schedule('@hourly', $$call partman.run_maintenance_proc()$$);
```
## Resources
- Official [pg_partman documentation](https://github.com/pgpartman/pg_partman/blob/development/doc/pg_partman.md)
@@ -8,6 +8,10 @@ description: 'Scalable time-series data storage and analysis'
The `timescaledb` extension is deprecated in projects using Postgres 17. It continues to be supported in projects using Postgres 15, but will need to dropped before those projects are upgraded to Postgres 17. See the [Upgrading to Postgres 17 notes](/docs/guides/platform/upgrading#upgrading-to-postgres-17) for more information.
If you are using hypertables, follow the [migration guide](/docs/guides/database/migrating-to-pg-partman) to convert to native partitioning managed by `pg_partman`.
For additional support, contact our Success team by creating a support ticket in the Supabase Dashboard.
</Admonition>
[`timescaledb`](https://docs.timescale.com/timescaledb/latest/) is a Postgres extension designed for improved handling of time-series data. It provides a scalable, high-performance solution for storing and querying time-series data on top of a standard Postgres database.
@@ -0,0 +1,113 @@
---
id: 'migrating-from-timescaledb-to-pg-partman'
title: 'Migrate from TimescaleDB to pg_partman'
description: 'Convert TimescaleDB hypertables to Postgres native partitions managed by pg_partman.'
---
Starting from Postgres 17, Supabase projects do not have the `timescaledb` extension available. If your project relies on TimescaleDB hypertables, you will need to migrate to standard Postgres tables before upgrading.
This guide shows one approach to migrate a hypertable to a native Postgres partitioned table and optionally configure `pg_partman` to automate ongoing partition maintenance.
The approach outlined in this guide can also be used for traditional partitioned tables.
## Before you begin
- Test the migration path in a staging environment (for example by creating a copy of your production project or using branching).
- Review your application for TimescaleDB-specific SQL usage (for example `time_bucket()`, compression policies). Those features are not provided by `pg_partman`.
## Migration overview
1. Create a new partitioned table.
2. Copy data from the hypertable to the new table.
3. Swap over and drop the hypertable.
4. Configure `pg_partman` (optional) and schedule maintenance.
## Example: Migrate `messages` from hypertable to native partitions
This example assumes a `messages` hypertable partitioned by `sent_at`.
### 1. Rename the existing hypertable
This keeps the original data in place while you create a new partitioned table with the original name.
{/* prettier-ignore */}
```sql
alter table public.messages rename to ht_messages;
```
### 2. Create a new partitioned table
When using native partitioning, the partitioning column must be included in any unique index (including the primary key).
{/* prettier-ignore */}
```sql
create table public.messages (
like public.ht_messages including all,
primary key (sent_at, id)
)
partition by range (sent_at);
```
### 3. Copy data into the new table
For large tables, consider copying in batches (for example by time range) during a maintenance window.
{/* prettier-ignore */}
```sql
insert into public.messages
select *
from public.ht_messages;
```
### 4. Drop the old hypertable (and TimescaleDB)
Only drop the extension once you’ve migrated all hypertables and no other objects depend on it.
{/* prettier-ignore */}
```sql
drop table public.ht_messages;
drop extension if exists timescaledb;
```
### 5. Configure `pg_partman` (optional)
Enable `pg_partman` and register your table so partitions are created ahead of time.
{/* prettier-ignore */}
```sql
create schema if not exists partman;
create extension if not exists pg_partman with schema partman;
select partman.create_parent(
p_parent_table := 'public.messages',
p_control := 'sent_at',
p_type := 'range',
p_interval := '7 days',
p_premake := 7,
p_start_partition := '2025-01-01 00:00:00'
);
```
## Keep partitions up to date
`pg_partman` requires running maintenance to pre-make partitions and apply retention policies.
{/* prettier-ignore */}
```sql
call partman.run_maintenance_proc();
```
To automate this, schedule it with `pg_cron`.
{/* prettier-ignore */}
```sql
create extension if not exists pg_cron;
select cron.schedule('@daily', $$call partman.run_maintenance_proc()$$);
```
## Additional resources
- [Partitioning your tables](/docs/guides/database/partitions).
- [`pg_partman` documentation](/docs/guides/database/extensions/pg_partman)
- [`pg_partman` migration guides](https://github.com/pgpartman/pg_partman/blob/development/doc/migrate_to_partman.md)
@@ -39,3 +39,5 @@ You can update scopes of your OAuth app at any time, but existing OAuth app user
| `Rest` | `Write` | Update a project's PostgREST configuration |
| `Secrets` | `Read` | Retrieve a project's API keys<br/>Retrieve a project's secrets<br/>Retrieve a project's pgsodium config |
| `Secrets` | `Write` | Create or update a project's secrets<br/>Update a project's pgsodium configuration |
| `Storage` | `Read` | Retrieve a project's storage buckets. |
| |
@@ -36,7 +36,7 @@ Usage is shown as "Log Drain Hours" on your invoice.
### Pricing
Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs <Price price="0.0822" /> per hour (<Price price="60" /> per month).
Log Drains are available as a project Add-On for all Pro, Team and Enterprise users. Each Log Drain costs <Price price="0.0822" /> per hour (<Price price="60" /> per month).
## Log Drain Events
+27 -27
View File
@@ -10,22 +10,22 @@ You can use Realtime Broadcast to send low-latency messages between users. Messa
The way Broadcast works changes based on the channel you are using:
- From REST API will receive an HTTP request which then will be sent via WebSocket to connected clients
- From Client libraries we have an established WebSocket connection and we use that to send a message to the server which then will be sent via WebSocket to connected clients
- From Database we add a new entry to `realtime.messages` where we have logical replication set to listen for changes which then will be sent via WebSocket to connected clients
- **REST API**: Receives an HTTP request and then sends a message via WebSocket to connected clients
- **Client libraries**: Sends a message via WebSocket to the server, and then the server sends a message via WebSocket to connected clients
- **Database**: Adds a new entry to `realtime.messages` where a logical replication is set to listen for changes, and then sends a message via WebSocket to connected clients
<Admonition type="note">
The public flag (the last argument in `realtime.send(payload, event, topic, is_private))` only affects who can subscribe to the topic not who can read messages from the database.
The public flag (the last argument in `realtime.send(payload, event, topic, is_private)`) only affects who can subscribe to the topic not who can read messages from the database.
- Public (false) → Anyone can subscribe to that topic without authentication
- Private (true) → Only authenticated clients can subscribe to that topic
- Public (`false`) → Anyone can subscribe to that topic without authentication
- Private (`true`) → Only authenticated clients can subscribe to that topic
However, regardless of whether it's public or private, the Realtime service connects to your database as the authenticated Supabase Admin role.
Regardless if it's public or private, the Realtime service connects to your database as the authenticated Supabase Admin role.
</Admonition>
For Authorization we insert a message and try to read it, and rollback the transaction to verify that the RLS policies set by the user are being respected by the user joining the channel, but this message isn't sent to the user. You can read more about it in the [Authorization docs](/docs/guides/realtime/authorization).
For Authorization, we insert a message and try to read it, and rollback the transaction to verify that the Row Level Security (RLS) policies set by the user are being respected by the user joining the channel, but this message isn't sent to the user. You can read more about it in [Authorization](/docs/guides/realtime/authorization).
## Subscribe to messages
@@ -132,9 +132,9 @@ In most cases, you can get the correct key from [the Project's **Connect** dialo
</$Show>
</Tabs>
### Receiving Broadcast messages
### Receive Broadcast messages
You can provide a callback for the `broadcast` channel to receive messages. This example will receive any `broadcast` messages that are sent to `test-channel`:
You can receive Broadcast messages by providing a callback to the channel.
<Tabs
scrollable
@@ -448,13 +448,13 @@ select
<Admonition type="note">
The realtime.send function in the database includes a flag that determines whether the broadcast is private or public, and client channels also have the same configuration. For broadcasts to work correctly, these settings must match a public broadcast will only reach public channels, and a private broadcast will only reach private ones.
The `realtime.send()` function in the database includes a flag that determines whether the broadcast is private or public, and client channels also have the same configuration. For broadcasts to work correctly, these settings must match. A public broadcast only reaches public channels and a private broadcast only reaches private channels.
By default, all database broadcasts are private, meaning clients must authenticate to receive them. If the database sends a public message but the client subscribes to a private channel, the message won't be delivered since private channels only accept signed, authenticated messages.
By default, all database broadcasts are private, meaning clients must authenticate to receive them. If the database sends a public message but the client subscribes to a private channel, the message is not delivered because private channels only accept signed, authenticated messages.
</Admonition>
It's a common use case to broadcast messages when a record is created, updated, or deleted. We provide a helper function specific to this use case, `realtime.broadcast_changes()`. For more details, check out the [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes) guide.
You can use the `realtime.broadcast_changes()` helper function to broadcast messages when a record is created, updated, or deleted. For more details, read [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes).
### Broadcast using the REST API
@@ -685,7 +685,7 @@ You can pass configuration options while initializing the Supabase Client.
>
<TabPanel id="js" label="JavaScript">
You can confirm that the Realtime servers have received your message by setting Broadcast's `ack` config to `true`.
You can confirm that the Realtime servers have received your message by setting Broadcast's `ack` setting to `true`.
{/* prettier-ignore */}
```js
@@ -881,20 +881,20 @@ You can also send a Broadcast message by making an HTTP request to Realtime serv
### How it works
Broadcast Changes allows you to trigger messages from your database. To achieve it Realtime is directly reading your WAL (Write Append Log) file using a publication against the `realtime.messages` table so whenever a new insert happens a message is sent to connected users.
Broadcast Changes allows you to trigger messages from your database. To achieve it, Realtime directly reads your Write-Ahead Log (WAL) file using a publication against the `realtime.messages` table. Whenever a new insert occurs, a message is sent to connected users.
It uses partitioned tables per day which allows the deletion your previous messages in a performant way by dropping the physical tables of this partitioned table. Tables older than 3 days old are deleted.
It uses partitioned tables per day, which allows performant deletion of your previous messages by dropping the physical tables of this partitioned table. Tables older than 3 days are deleted.
Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON packages. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data.
Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON payloads. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data.
The database broadcast feature provides two functions to help you send messages:
Broadcast Changes provides two functions to help you send messages:
- `realtime.send` will insert a message into realtime.messages without a specific format.
- `realtime.broadcast_changes` will insert a message with the required fields to emit database changes to clients. This helps you set up triggers on your tables to emit changes.
- `realtime.send()` inserts a message into `realtime.messages` without a specific format.
- `realtime.broadcast_changes()` inserts a message with the required fields to emit database changes to clients. This helps you set up triggers on your tables to emit changes.
### Broadcasting a message from your database
The `realtime.send` function provides the most flexibility by allowing you to broadcast messages from your database without a specific format. This allows you to use database broadcast for messages that aren't necessarily tied to the shape of a Postgres row change.
The `realtime.send()` function provides the most flexibility by allowing you to broadcast messages from your database without a specific format. This allows you to use database broadcast for messages that aren't necessarily tied to the shape of a Postgres row change.
```sql
SELECT realtime.send (
@@ -909,7 +909,7 @@ SELECT realtime.send (
#### Setup realtime authorization
Realtime Authorization is required and enabled by default. To allow your users to listen to messages from topics, create an RLS (Row Level Security) policy:
Realtime Authorization is required and enabled by default. To allow your users to listen to messages from topics, create an RLS policy:
```sql
CREATE POLICY "authenticated can receive broadcasts"
@@ -920,13 +920,13 @@ USING ( true );
```
See the [Realtime Authorization](/docs/guides/realtime/authorization) docs to learn how to set up more specific policies.
Read [Realtime Authorization](/docs/guides/realtime/authorization) to learn how to set up more specific policies.
#### Set up trigger function
First, set up a trigger function that uses `realtime.broadcast_changes` to insert an event whenever it is triggered. The event is set up to include data on the schema, table, operation, and field changes that triggered it.
First, set up a trigger function that uses the `realtime.broadcast_changes()` function to insert an event whenever it is triggered. The event is set up to include data on the schema, table, operation, and field changes that triggered it.
For this example use case, we want to have a topic with the name `topic:<record id>` to which we're going to broadcast events.
For this example, you're going broadcast events to a topic named `topic:<record_id>`.
```sql
CREATE OR REPLACE FUNCTION public.your_table_changes()
@@ -948,7 +948,7 @@ END;
$$ LANGUAGE plpgsql;
```
Of note are the Postgres native trigger special variables used:
The Postgres native trigger special variables used are:
- `TG_OP` - the operation that triggered the function
- `TG_TABLE_NAME` - the table that caused the trigger
@@ -1000,7 +1000,7 @@ Broadcast Replay enables **private** channels to access messages that were sent
You can configure replay with the following options:
- **`since`** (Required): The epoch timestamp in milliseconds (e.g., `1697472000000`), specifying the earliest point from which messages should be retrieved.
- **`since`** (Required): The epoch timestamp in milliseconds (for example, `1697472000000`), specifying the earliest point from which messages should be retrieved.
- **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25.
<Tabs
@@ -4,7 +4,7 @@ title: 'Log Drains'
description: 'Getting started with Supabase Log Drains'
---
Log drains will send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Team and Enterprise Plans. Log drains is available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains).
Log drains send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Pro, Team and Enterprise Plans. Log drains are available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains).
You can read about the initial announcement [here](/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top).
@@ -29,6 +29,7 @@ import { QueueOperationsPreview } from './QueueOperationsPreview'
import { TableFilterBarPreview } from './TableFilterBarPreview'
import { UnifiedLogsPreview } from './UnifiedLogsPreview'
import { useFeaturePreviews } from './useFeaturePreviews'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[key: string]: ReactNode
@@ -56,6 +57,11 @@ export const FeaturePreviewModal = () => {
const featurePreviewContext = useFeaturePreviewContext()
const { mutate: sendEvent } = useSendEventMutation()
const [isDismissedTableFilterBar, setIsDismissedTableFilterBar] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''),
false
)
const { flags, onUpdateFlag } = featurePreviewContext
const selectedFeature =
featurePreviews.find((preview) => preview.key === selectedFeatureKey) ?? featurePreviews[0]
@@ -72,6 +78,13 @@ export const FeaturePreviewModal = () => {
properties: { feature: selectedFeature.key },
groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
if (
selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR &&
!isDismissedTableFilterBar
) {
setIsDismissedTableFilterBar(true)
}
}
return (
@@ -1,11 +1,4 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
import { useParams } from 'common'
import { Markdown } from 'components/interfaces/Markdown'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
@@ -13,17 +6,23 @@ import { DocsButton } from 'components/ui/DocsButton'
import { ResourceItem } from 'components/ui/Resource/ResourceItem'
import type { components } from 'data/api'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { BASE_PATH } from 'lib/constants'
import { Check } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui'
import { Admonition } from 'ui-patterns'
import { NO_REQUIRED_CHARACTERS } from '../Auth.constants'
import { AuthAlert } from './AuthAlert'
import type { Provider } from './AuthProvidersForm.types'
import FormField from './FormField'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
interface ProviderFormProps {
config: components['schemas']['GoTrueConfigResponse']
@@ -42,6 +41,8 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
const [open, setOpen] = useState(false)
const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation()
const { data: endpoint } = useProjectApiUrl({ projectRef })
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
@@ -67,12 +68,6 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
}
const isFreePlan = organization?.plan.id === 'free'
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const apiUrl = `${protocol}://${endpoint}`
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const INITIAL_VALUES = (() => {
const initialValues: { [x: string]: string | boolean } = {}
@@ -247,11 +242,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
readOnly
disabled
label="Callback URL (for OAuth)"
value={
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}/auth/v1/callback`
: `${apiUrl}/auth/v1/callback`
}
value={`${endpoint}/auth/v1/callback`}
descriptionText={
<Markdown
content={provider.misc.helper}
@@ -1,10 +1,4 @@
import { zodResolver } from '@hookform/resolvers/zod'
import randomBytes from 'randombytes'
import { useEffect, useMemo } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'
import { useParams } from 'common'
import { convertArgumentTypes } from 'components/interfaces/Database/Functions/Functions.utils'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
@@ -16,11 +10,15 @@ import { useAuthHooksUpdateMutation } from 'data/auth/auth-hooks-update-mutation
import { executeSql } from 'data/sql/execute-sql-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import randomBytes from 'randombytes'
import { useEffect, useMemo } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
RadioGroupStacked,
RadioGroupStackedItem,
@@ -35,7 +33,9 @@ import {
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { HOOKS_DEFINITIONS, HOOK_DEFINITION_TITLE, Hook } from './hooks.constants'
import * as z from 'zod'
import { Hook, HOOK_DEFINITION_TITLE, HOOKS_DEFINITIONS } from './hooks.constants'
import { extractMethod, getRevokePermissionStatements, isValidHook } from './hooks.utils'
interface CreateHookSheetProps {
@@ -239,7 +239,7 @@ export const CreateHookSheet = ({
form.reset({
hookType: definition.title,
enabled: authConfig?.[definition.enabledKey] || true,
enabled: isCreating ? true : authConfig?.[definition.enabledKey],
selectedType: values.type,
httpsValues: {
url: (values.type === 'https' && values.url) || '',
@@ -7,7 +7,7 @@ import type {
import { useParams } from 'common'
import { InlineLink } from 'components/ui/InlineLink'
import Panel from 'components/ui/Panel'
import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
import { useOAuthServerAppCreateMutation } from 'data/oauth-server-apps/oauth-server-app-create-mutation'
import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation'
import { useOAuthServerAppUpdateMutation } from 'data/oauth-server-apps/oauth-server-app-update-mutation'
@@ -111,7 +111,7 @@ export const CreateOrUpdateOAuthAppSheet = ({
control: form.control,
})
const { data: endpointData } = useProjectEndpointQuery({ projectRef })
const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef })
const { mutate: createOAuthApp, isPending: isCreating } = useOAuthServerAppCreateMutation({
onSuccess: (data) => {
@@ -201,8 +201,8 @@ export const CreateOrUpdateOAuthAppSheet = ({
updateOAuthApp({
projectRef,
clientEndpoint,
clientId: appToEdit.client_id,
clientEndpoint: endpointData?.endpoint,
...payload,
})
} else {
@@ -216,7 +216,7 @@ export const CreateOrUpdateOAuthAppSheet = ({
createOAuthApp({
projectRef,
clientEndpoint: endpointData?.endpoint,
clientEndpoint,
...payload,
})
}
@@ -234,8 +234,8 @@ export const CreateOrUpdateOAuthAppSheet = ({
const handleConfirmRegenerate = () => {
regenerateSecret({
projectRef,
clientEndpoint,
clientId: appToEdit?.client_id,
clientEndpoint: endpointData?.endpoint,
})
}
@@ -1,6 +1,6 @@
import type { OAuthClient } from '@supabase/supabase-js'
import { useParams } from 'common'
import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
import type { OAuthServerAppDeleteVariables } from 'data/oauth-server-apps/oauth-server-app-delete-mutation'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -21,12 +21,13 @@ export const DeleteOAuthAppModal = ({
}: DeleteOAuthAppModalProps) => {
const { ref: projectRef } = useParams()
const { data: endpointData } = useProjectEndpointQuery({ projectRef })
const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef })
const onConfirmDeleteApp = () => {
onDelete({
projectRef,
clientEndpoint,
clientId: selectedApp?.client_id,
clientEndpoint: endpointData?.endpoint,
})
}
@@ -4,7 +4,7 @@ import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { FilterPopover } from 'components/ui/FilterPopover'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
import { useOAuthServerAppDeleteMutation } from 'data/oauth-server-apps/oauth-server-app-delete-mutation'
import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation'
import { useOAuthServerAppsQuery } from 'data/oauth-server-apps/oauth-server-apps-query'
@@ -75,7 +75,7 @@ export const OAuthAppsList = () => {
const [filteredClientTypes, setFilteredClientTypes] = useState<string[]>([])
const [filterString, setFilterString] = useState<string>('')
const { data: endpointData } = useProjectEndpointQuery({ projectRef })
const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef })
const {
data,
error,
@@ -457,8 +457,8 @@ export const OAuthAppsList = () => {
onConfirm={() => {
regenerateSecret({
projectRef,
clientEndpoint,
clientId: selectedApp?.client_id,
clientEndpoint: endpointData?.endpoint,
})
setShowRegenerateDialog(false)
}}
@@ -1,12 +1,12 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { AlertCircle, ExternalLink, Loader2 } from 'lucide-react'
import Link from 'next/link'
import type { ReactNode } from 'react'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Button } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import type { projectKeys } from './Connect.types'
function KeyRow({ label, value }: { label: ReactNode; value: string }) {
@@ -63,7 +63,6 @@ export function ApiKeysTabContent({ projectKeys }: { projectKeys: projectKeys })
value={projectKeys.anonKey ?? ''}
/>
{/* Footer */}
<div className="gap-5 lg:grid lg:grid-cols-12">
<div className="col-start-5 col-span-8 pl-2 flex items-center justify-between">
<p className="text-xs text-foreground-lighter">For secret keys, see API settings.</p>
@@ -5,7 +5,6 @@ import { DatabaseConnectionString } from 'components/interfaces/Connect/Database
import { McpTabContent } from 'components/interfaces/Connect/McpTabContent'
import Panel from 'components/ui/Panel'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { BASE_PATH } from 'lib/constants'
@@ -35,6 +34,7 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C
import { getContentFilePath, inferConnectTabFromParentKey } from './Connect.utils'
import { ConnectDropdown } from './ConnectDropdown'
import { ConnectTabContent } from './ConnectTabContent'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
export const Connect = () => {
const router = useRouter()
@@ -97,7 +97,8 @@ export const Connect = () => {
?.children.find((child) => child.key === selectedChild)?.children[0]?.key || ''
)
const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: open })
const { data: resolvedEndpoint } = useProjectApiUrl({ projectRef })
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
@@ -216,22 +217,12 @@ export const Connect = () => {
: { anonKey: null, publishableKey: null }
const projectKeys = useMemo(() => {
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : ''
return {
apiUrl: apiHost ?? null,
apiUrl: resolvedEndpoint ?? null,
anonKey: anonKey?.api_key ?? null,
publishableKey: publishableKey?.api_key ?? null,
}
}, [
settings?.app_config?.protocol,
settings?.app_config?.endpoint,
canReadAPIKeys,
anonKey?.api_key,
publishableKey?.api_key,
])
}, [resolvedEndpoint, anonKey?.api_key, publishableKey?.api_key])
const filePath = getContentFilePath({
connectionObject,
@@ -1,7 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useMemo } from 'react'
@@ -11,9 +10,12 @@ import type { ProjectKeys } from './Connect.types'
import { ConnectConfigSection, ModeSelector } from './ConnectConfigSection'
import { ConnectStepsSection } from './ConnectStepsSection'
import { useConnectState } from './useConnectState'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
export const ConnectSheet = () => {
const { ref: projectRef } = useParams()
const {
projectConnectionShowAppFrameworks: showAppFrameworks,
projectConnectionShowMobileFrameworks: showMobileFrameworks,
@@ -40,9 +42,8 @@ export const ConnectSheet = () => {
const { state, activeFields, resolvedSteps, schema, getFieldOptions, setMode, updateField } =
useConnectState()
// Project keys for step components
const { ref: projectRef } = useParams()
const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: showConnect })
const { data: endpoint = '' } = useProjectApiUrl({ projectRef }, { enabled: showConnect })
const { can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
@@ -53,22 +54,12 @@ export const ConnectSheet = () => {
: { anonKey: null, publishableKey: null }
const projectKeys: ProjectKeys = useMemo(() => {
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : ''
return {
apiUrl: apiHost ?? null,
apiUrl: endpoint,
anonKey: anonKey?.api_key ?? null,
publishableKey: publishableKey?.api_key ?? null,
}
}, [
settings?.app_config?.protocol,
settings?.app_config?.endpoint,
canReadAPIKeys,
anonKey?.api_key,
publishableKey?.api_key,
])
}, [endpoint, anonKey?.api_key, publishableKey?.api_key])
const availableModeIds = useMemo(() => {
const modes: string[] = []
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import { useParams } from 'common'
import { CreateAnalyticsBucketSheet } from 'components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -35,6 +35,10 @@ import {
import { Button, DialogSectionSeparator, Form_Shadcn_, SheetFooter, SheetSection } from 'ui'
import * as z from 'zod'
import {
useIsETLBigQueryPrivateAlpha,
useIsETLIcebergPrivateAlpha,
} from '../../useIsETLPrivateAlpha'
import { DestinationType } from '../DestinationPanel.types'
import { AdvancedSettings } from './AdvancedSettings'
import { CREATE_NEW_NAMESPACE } from './DestinationForm.constants'
@@ -75,8 +79,8 @@ export const DestinationForm = ({
const { ref: projectRef } = useParams()
const { setRequestStatus } = usePipelineRequestStatus()
const etlEnableBigQuery = useFlag('etlEnableBigQuery')
const etlEnableIceberg = useFlag('etlEnableIceberg')
const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const [isFormInteracting, setIsFormInteracting] = useState(false)
@@ -7,6 +7,7 @@ import { useEffect } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
DialogSectionSeparator,
Sheet,
SheetContent,
@@ -14,7 +15,6 @@ import {
SheetHeader,
SheetSection,
SheetTitle,
cn,
} from 'ui'
import { EnableReplicationCallout } from '../EnableReplicationCallout'
@@ -1,18 +1,17 @@
import { useFlag } from 'common'
import { AnalyticsBucket, BigQuery, Database } from 'icons'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import { Badge, RadioGroupStacked, RadioGroupStackedItem, cn } from 'ui'
import { Badge, cn, RadioGroupStacked, RadioGroupStackedItem } from 'ui'
import { useDestinationInformation } from '../useDestinationInformation'
import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
import { useIsETLBigQueryPrivateAlpha, useIsETLIcebergPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationType } from './DestinationPanel.types'
import { InlineLink } from '@/components/ui/InlineLink'
export const DestinationTypeSelection = () => {
const enablePgReplicate = useIsETLPrivateAlpha()
const unifiedReplication = useFlag('unifiedReplication')
const etlEnableBigQuery = useFlag('etlEnableBigQuery')
const etlEnableIceberg = useFlag('etlEnableIceberg')
const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
const numberOfTypes = [unifiedReplication, etlEnableBigQuery, etlEnableIceberg].filter(
Boolean
@@ -52,8 +51,8 @@ export const DestinationTypeSelection = () => {
value={destinationType}
onValueChange={(value) => setDestinationType(value as DestinationType)}
className={cn(
'grid [&>button>div]:py-4',
numberOfTypes === 3 ? 'grid-cols-3' : numberOfTypes === 2 ? 'grid-cols-2' : 'grid-cols-1',
'grid [&>button>div]:py-4 grid-cols-3',
numberOfTypes === 3 && !editMode ? 'grid-cols-3' : 'grid-cols-2',
'[&>button:first-of-type]:rounded-none [&>button:last-of-type]:rounded-none',
'[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg'
)}
@@ -121,7 +120,7 @@ export const DestinationTypeSelection = () => {
)}
</RadioGroupStacked>
{destinationType !== 'Read Replica' && enablePgReplicate && (
{destinationType !== 'Read Replica' && (
<p className="mt-3 text-sm text-foreground-light">
Replication is in alpha. Expect rapid changes and possible breaking updates.{' '}
<InlineLink href="https://github.com/orgs/supabase/discussions/39416">
@@ -2,18 +2,37 @@ import { useFlag } from 'common'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
/**
* Organization level opt in for ETL private alpha
* Organization level opt in for ETL private alpha, there's 2 flags we're using which controls
* the individual destination types. Access to the ETL UI (`useIsETLPrivateAlpha`) will just
* check if the org has access to at least one of the destination types
*/
export const useIsETLPrivateAlpha = () => {
const useIsCurrentOrgInFlagList = (flag: string) => {
const flagValue = useFlag(flag)
const { data: organization } = useSelectedOrganizationQuery()
const etlPrivateAlpha = useFlag('etlPrivateAlpha')
const privateAlphaOrgSlugs =
typeof etlPrivateAlpha === 'string'
? (etlPrivateAlpha as string).split(',').map((x) => x.trim())
const allowedOrgSlugs =
typeof flagValue === 'string'
? (flagValue as string).split(',').map((x: string) => x.trim())
: []
const etlShowForAllProjects = useFlag('etlPrivateAlphaOverride')
// [Joshen] Override for to enable for all organizations by setting the flag value as `all`
if (allowedOrgSlugs.includes('all')) return true
return etlShowForAllProjects || privateAlphaOrgSlugs.includes(organization?.slug ?? '')
// [Joshen] Otherwise fallback to checking against org slug
return allowedOrgSlugs.includes(organization?.slug ?? '')
}
export const useIsETLBigQueryPrivateAlpha = () => {
return useIsCurrentOrgInFlagList('etlEnableBigQueryPrivateAlpha')
}
export const useIsETLIcebergPrivateAlpha = () => {
return useIsCurrentOrgInFlagList('etlEnableIcebergPrivateAlpha')
}
export const useIsETLPrivateAlpha = () => {
const hasAccessToETLBigQuery = useIsCurrentOrgInFlagList('etlEnableBigQueryPrivateAlpha')
const hasAccessToETLIceberg = useIsCurrentOrgInFlagList('etlEnableIcebergPrivateAlpha')
return hasAccessToETLBigQuery || hasAccessToETLIceberg
}
@@ -7,13 +7,12 @@ import Description from '@/components/interfaces/Docs/Description'
import Param from '@/components/interfaces/Docs/Param'
import Snippets from '@/components/interfaces/Docs/Snippets'
import { InlineLink } from '@/components/ui/InlineLink'
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useProjectJsonSchemaQuery } from '@/data/docs/project-json-schema-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { DOCS_URL } from '@/lib/constants'
interface ResourceContentProps {
apiEndpoint: string
resourceId: string
resources: { [key: string]: { id: string; displayName: string; camelCase: string } }
selectedLang: 'bash' | 'js'
@@ -22,7 +21,6 @@ interface ResourceContentProps {
}
export const ResourceContent = ({
apiEndpoint,
resourceId,
resources,
selectedLang,
@@ -30,16 +28,12 @@ export const ResourceContent = ({
refreshDocs,
}: ResourceContentProps) => {
const { ref } = useParams()
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all'])
const { data: jsonSchema } = useProjectJsonSchemaQuery({ projectRef: ref })
const { paths, definitions } = jsonSchema || {}
const endpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}`
: apiEndpoint
const { data: endpoint = '' } = useProjectApiUrl({ projectRef: ref })
const keyToShow = !!showApiKey ? showApiKey : 'SUPABASE_KEY'
const resourcePaths = paths?.[`/${resourceId}`]
@@ -9,29 +9,30 @@ import { useEffect, useMemo, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Card,
CardContent,
CardFooter,
cn,
CodeBlock,
copyToClipboard,
CriticalIcon,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Switch,
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
cn,
copyToClipboard,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageSection,
@@ -40,7 +41,6 @@ import {
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import z from 'zod'
import CommandRender from '../CommandRender'
@@ -48,8 +48,7 @@ import { INVOCATION_TABS } from './EdgeFunctionDetails.constants'
import { generateCLICommands } from './EdgeFunctionDetails.utils'
import AlertError from '@/components/ui/AlertError'
import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functions-delete-mutation'
import { useEdgeFunctionUpdateMutation } from '@/data/edge-functions/edge-functions-update-mutation'
@@ -86,24 +85,18 @@ export const EdgeFunctionDetails = () => {
const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const { data: apiKeys } = useAPIKeysQuery(
{
projectRef,
},
{ enabled: canReadAPIKeys }
)
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const {
data: selectedFunction,
error,
isPending: isLoading,
isError,
isSuccess,
} = useEdgeFunctionQuery({
projectRef,
slug: functionSlug,
})
} = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
const { data: endpoint } = useProjectApiUrl({ projectRef })
const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}`
const { mutate: updateEdgeFunction, isPending: isUpdating } = useEdgeFunctionUpdateMutation()
const { mutate: deleteEdgeFunction, isPending: isDeleting } = useEdgeFunctionDeleteMutation({
@@ -121,12 +114,6 @@ export const EdgeFunctionDetails = () => {
const { anonKey, publishableKey } = getKeys(apiKeys)
const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const functionUrl =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}/functions/v1/${selectedFunction?.slug}`
: `${protocol}://${endpoint}/functions/v1/${selectedFunction?.slug}`
const hasImportMap = useMemo(
() => selectedFunction?.import_map || selectedFunction?.import_map_path,
[selectedFunction]
@@ -4,11 +4,10 @@ import dayjs from 'dayjs'
import { Check, Copy } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { TableCell, TableRow, copyToClipboard } from 'ui'
import { copyToClipboard, TableCell, TableRow } from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import type { EdgeFunctionsResponse } from '@/data/edge-functions/edge-functions-query'
import { createNavigationHandler } from '@/lib/navigation'
@@ -21,15 +20,8 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
const { ref } = useParams()
const [isCopied, setIsCopied] = useState(false)
const { data: settings } = useProjectSettingsV2Query({ projectRef: ref })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: ref })
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const functionUrl =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}/functions/v1/${item.slug}`
: `${protocol}://${endpoint}/functions/v1/${item.slug}`
const { data: endpoint } = useProjectApiUrl({ projectRef: ref })
const functionUrl = `${endpoint}/functions/v1/${item.slug}`
const handleNavigation = createNavigationHandler(
`/project/${ref}/functions/${item.slug}${IS_PLATFORM ? '' : `/details`}`,
@@ -1,24 +1,23 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ExternalLink, Maximize2, Minimize2, Terminal } from 'lucide-react'
import { useRouter } from 'next/router'
import { ComponentPropsWithoutRef, ElementRef, forwardRef, useState } from 'react'
import { useParams } from 'common'
import CommandRender from 'components/interfaces/Functions/CommandRender'
import { DocsButton } from 'components/ui/DocsButton'
import { useAccessTokensQuery } from 'data/access-tokens/access-tokens-query'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { DOCS_URL } from 'lib/constants'
import { ExternalLink, Maximize2, Minimize2, Terminal } from 'lucide-react'
import { useRouter } from 'next/router'
import { ComponentPropsWithoutRef, ElementRef, forwardRef, useState } from 'react'
import {
Button,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
} from 'ui'
import type { Commands } from './Functions.types'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
interface TerminalInstructionsProps extends ComponentPropsWithoutRef<typeof Collapsible_Shadcn_> {
closable?: boolean
@@ -36,19 +35,13 @@ export const TerminalInstructions = forwardRef<
const { data: tokens } = useAccessTokensQuery()
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const { data: endpoint } = useProjectApiUrl({ projectRef })
const functionsEndpoint = `${endpoint}/functions/v1`
const { anonKey, publishableKey } = getKeys(apiKeys)
const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint ?? ''
const functionsEndpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain.hostname}/functions/v1`
: `${protocol}://${endpoint}/functions/v1`
// get the .co or .net TLD from the restUrl
const restUrl = `https://${endpoint}`
const restUrlTld = !!endpoint ? new URL(restUrl).hostname.split('.').pop() : 'co'
@@ -2,7 +2,6 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Label } from '@ui/components/shadcn/ui/label'
import { getConnectionStrings } from 'components/interfaces/Connect/DatabaseSettings.utils'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { pluckObjectFields } from 'lib/helpers'
@@ -10,8 +9,11 @@ import { Plug } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useMemo, useState, type ReactNode } from 'react'
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
import { ShimmeringLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user'] as const
const EMPTY_CONNECTION_INFO = {
db_user: '',
@@ -37,32 +39,23 @@ export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHove
const [open, setOpen] = useState(false)
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const { data: settings, isPending: isLoadingSettings } = useProjectSettingsV2Query(
{ projectRef },
{ enabled: !!projectRef }
)
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const projectUrl = endpoint ? `${protocol}://${endpoint}` : undefined
const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const { data: projectUrl, isPending: isLoadingApiUrl } = useProjectApiUrl({ projectRef })
const { data: apiKeys, isLoading: isLoadingKeys } = useAPIKeysQuery(
{ projectRef },
{ enabled: open && canReadAPIKeys }
)
const { publishableKey } = canReadAPIKeys ? getKeys(apiKeys) : { publishableKey: null }
const { data: databases, isLoading: isLoadingDatabases } = useReadReplicasQuery(
{ projectRef },
{ enabled: open && !!projectRef }
)
const primaryDatabase = databases?.find((db) => db.identifier === projectRef)
const directConnectionString = useMemo(() => {
@@ -81,9 +74,6 @@ export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHove
}).direct.uri
}, [primaryDatabase, projectRef])
const projectUrlLabel =
projectUrl ?? (isLoadingSettings ? 'Loading project URL...' : 'Project URL unavailable')
return (
<HoverCard openDelay={250} closeDelay={100} open={open} onOpenChange={setOpen}>
<HoverCardTrigger asChild>
@@ -91,9 +81,13 @@ export const ProjectConnectionHoverCard = ({ projectRef }: ProjectConnectionHove
<div className=" transition w-8 h-8 rounded-md bg-surface-75 group-hover:bg-muted border flex items-center justify-center">
<Plug strokeWidth={1.5} size={16} className="text-foreground-light rotate-90" />
</div>
<span className="transition text-foreground-light group-hover:text-foreground underline decoration-dotted decoration-foreground-muted underline-offset-4 max-w-[320px] text-left">
{projectUrlLabel}
</span>
{isLoadingApiUrl ? (
<ShimmeringLoader className="w-32" />
) : (
<span className="transition text-foreground-light group-hover:text-foreground underline decoration-dotted decoration-foreground-muted underline-offset-4 max-w-[320px] text-left">
{projectUrl ?? 'Project URL unavailable'}
</span>
)}
</button>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-[420px] p-0">
@@ -1,28 +1,10 @@
import { describe, expect, it } from 'vitest'
import { buildEntityMaps, getApiEndpoint, getProjectApiEndpoint } from './DataApi.utils'
import type { ProjectSettings } from '@/data/config/project-settings-v2-query'
import type {
CustomDomainResponse,
CustomDomainsData,
} from '@/data/custom-domains/custom-domains-query'
import { buildEntityMaps, getApiEndpoint } from './DataApi.utils'
import type { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query'
import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query'
import type { Database } from '@/data/read-replicas/replicas-query'
const makeCustomDomainData = (hostname: string): CustomDomainsData => ({
customDomain: {
id: '',
ssl: {} as CustomDomainResponse['ssl'],
hostname,
status: 'active',
created_at: '',
custom_metadata: null,
custom_origin_server: '',
},
status: '5_services_reconfigured',
})
const makeDatabase = (
identifier: string,
restUrl: string
@@ -30,71 +12,13 @@ const makeDatabase = (
const makeLoadBalancer = (endpoint: string): Pick<LoadBalancer, 'endpoint'> => ({ endpoint })
describe('getProjectApiEndpoint', () => {
it('returns custom domain URL when custom domain is active', () => {
expect(
getProjectApiEndpoint({
settings: undefined,
customDomainData: makeCustomDomainData('api.example.com'),
})
).toBe('https://api.example.com')
})
it('returns settings-based URL when no custom domain', () => {
expect(
getProjectApiEndpoint({
settings: {
app_config: { protocol: 'https', endpoint: 'abc.supabase.co' },
} as ProjectSettings,
customDomainData: undefined,
})
).toBe('https://abc.supabase.co')
})
it('respects protocol from settings', () => {
expect(
getProjectApiEndpoint({
settings: {
app_config: { protocol: 'http', endpoint: 'localhost:54321' },
} as ProjectSettings,
customDomainData: undefined,
})
).toBe('http://localhost:54321')
})
it('returns placeholder when settings are undefined', () => {
expect(
getProjectApiEndpoint({
settings: undefined,
customDomainData: undefined,
})
).toBe('https://-')
})
it('ignores inactive custom domain', () => {
const inactiveCustomDomain: CustomDomainsData = {
customDomain: null,
status: '0_no_hostname_configured',
}
expect(
getProjectApiEndpoint({
settings: {
app_config: { protocol: 'https', endpoint: 'abc.supabase.co' },
} as ProjectSettings,
customDomainData: inactiveCustomDomain,
})
).toBe('https://abc.supabase.co')
})
})
describe('getApiEndpoint', () => {
it('returns custom domain URL when custom domain is active and primary database is selected', () => {
expect(
getApiEndpoint({
selectedDatabaseId: 'project-ref',
projectRef: 'project-ref',
customDomainData: makeCustomDomainData('api.example.com'),
resolvedEndpoint: 'https://api.example.com',
loadBalancers: undefined,
selectedDatabase: makeDatabase(
'project-ref',
@@ -109,7 +33,7 @@ describe('getApiEndpoint', () => {
getApiEndpoint({
selectedDatabaseId: 'replica-1',
projectRef: 'project-ref',
customDomainData: makeCustomDomainData('api.example.com'),
resolvedEndpoint: 'https://api.example.com',
loadBalancers: undefined,
selectedDatabase: makeDatabase(
'replica-1',
@@ -124,7 +48,7 @@ describe('getApiEndpoint', () => {
getApiEndpoint({
selectedDatabaseId: 'load-balancer',
projectRef: 'project-ref',
customDomainData: undefined,
resolvedEndpoint: 'https://project-ref.supabase.co',
loadBalancers: [makeLoadBalancer('https://lb.supabase.co') as LoadBalancer],
selectedDatabase: undefined,
})
@@ -136,46 +60,26 @@ describe('getApiEndpoint', () => {
getApiEndpoint({
selectedDatabaseId: 'load-balancer',
projectRef: 'project-ref',
customDomainData: undefined,
resolvedEndpoint: 'https://project-ref.supabase.co',
loadBalancers: undefined,
selectedDatabase: undefined,
})
).toBe('')
})
it('returns database restUrl for a normal database selection', () => {
it('returns database restUrl for a replica database selection', () => {
expect(
getApiEndpoint({
selectedDatabaseId: 'project-ref',
selectedDatabaseId: 'replica-2',
projectRef: 'project-ref',
customDomainData: undefined,
resolvedEndpoint: 'https://project-ref.supabase.co',
loadBalancers: undefined,
selectedDatabase: makeDatabase(
'project-ref',
'https://project-ref.supabase.co/rest/v1'
'replica-2',
'https://replica-2.supabase.co/rest/v1'
) as Database,
})
).toBe('https://project-ref.supabase.co/rest/v1')
})
it('ignores custom domain when it is not active', () => {
const inactiveCustomDomain: CustomDomainsData = {
customDomain: null,
status: '0_no_hostname_configured',
}
expect(
getApiEndpoint({
selectedDatabaseId: 'project-ref',
projectRef: 'project-ref',
customDomainData: inactiveCustomDomain,
loadBalancers: undefined,
selectedDatabase: makeDatabase(
'project-ref',
'https://project-ref.supabase.co/rest/v1'
) as Database,
})
).toBe('https://project-ref.supabase.co/rest/v1')
).toBe('https://replica-2.supabase.co/rest/v1')
})
})
@@ -1,29 +1,8 @@
import type { ProjectSettings } from '@/data/config/project-settings-v2-query'
import type { CustomDomainsData } from '@/data/custom-domains/custom-domains-query'
import type { ProjectJsonSchemaPaths } from '@/data/docs/project-json-schema-query'
import type { LoadBalancer } from '@/data/read-replicas/load-balancers-query'
import type { Database } from '@/data/read-replicas/replicas-query'
import { snakeToCamel } from '@/lib/helpers'
/**
* Resolves the primary project API endpoint, respecting custom domains.
*/
export function getProjectApiEndpoint({
settings,
customDomainData,
}: {
settings: ProjectSettings | undefined
customDomainData: CustomDomainsData | undefined
}): string {
if (customDomainData?.customDomain?.status === 'active') {
return `https://${customDomainData.customDomain.hostname}`
}
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
return `${protocol}://${endpoint ?? '-'}`
}
/**
* Resolves the API endpoint URL based on the selected database, custom domain
* status, and load balancer configuration.
@@ -31,21 +10,20 @@ export function getProjectApiEndpoint({
export function getApiEndpoint({
selectedDatabaseId,
projectRef,
customDomainData,
resolvedEndpoint,
loadBalancers,
selectedDatabase,
}: {
selectedDatabaseId: string | undefined
projectRef: string | undefined
customDomainData: CustomDomainsData | undefined
resolvedEndpoint: string | undefined
loadBalancers: Array<LoadBalancer> | undefined
selectedDatabase: Database | undefined
}): string {
const isCustomDomainActive = customDomainData?.customDomain?.status === 'active'
const loadBalancerSelected = selectedDatabaseId === 'load-balancer'
if (isCustomDomainActive && selectedDatabaseId === projectRef) {
return `https://${customDomainData.customDomain.hostname}`
if (selectedDatabaseId === projectRef && !!resolvedEndpoint) {
return resolvedEndpoint
}
if (loadBalancerSelected) {
@@ -4,14 +4,10 @@ import type { ShowApiKey } from '../../Docs/Docs.types'
import { GeneralContent } from '@/components/interfaces/Docs/GeneralContent'
import { ResourceContent } from '@/components/interfaces/Docs/ResourceContent'
import { RpcContent } from '@/components/interfaces/Docs/RpcContent'
import {
buildEntityMaps,
getProjectApiEndpoint,
} from '@/components/interfaces/Integrations/DataApi/DataApi.utils'
import { buildEntityMaps } from '@/components/interfaces/Integrations/DataApi/DataApi.utils'
import { DocViewError } from '@/components/interfaces/Integrations/DataApi/DocViewError'
import { DocViewLoading } from '@/components/interfaces/Integrations/DataApi/DocViewLoading'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
import { useProjectJsonSchemaQuery } from '@/data/docs/project-json-schema-query'
interface DocViewProps {
@@ -29,9 +25,6 @@ export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
isPending: isLoading,
refetch,
} = useProjectJsonSchemaQuery({ projectRef })
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const endpoint = getProjectApiEndpoint({ settings, customDomainData })
const { paths } = jsonSchema || {}
const PAGE_KEY = resource || rpc || page || 'index'
@@ -51,7 +44,6 @@ export const DocView = ({ selectedLang, selectedApiKey }: DocViewProps) => {
<div className="flex-1 flex flex-col">
{resource ? (
<ResourceContent
apiEndpoint={endpoint}
selectedLang={selectedLang}
resourceId={resource}
resources={resources}
@@ -163,7 +163,7 @@ export const AuditLogs = () => {
if (isLogsNotAvailableBasedOnPlan) {
return (
<ScaffoldContainer>
<ScaffoldContainer className="px-6 xl:px-10">
<ScaffoldSection isFullWidth>
<UpgradeToPro
plan="Team"
@@ -179,7 +179,7 @@ export const AuditLogs = () => {
return (
<>
<ScaffoldContainer>
<ScaffoldContainer className="px-6 xl:px-10">
<ScaffoldSection isFullWidth>
<div className="space-y-4 flex flex-col">
{showFilters && (
@@ -1,10 +1,12 @@
import { NoProjectsOnPaidOrgInfo } from 'components/interfaces/Billing/NoProjectsOnPaidOrgInfo'
import {
ScaffoldContainer,
ScaffoldSection,
ScaffoldSectionTitle,
} from 'components/layouts/Scaffold'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { OrganizationDeletePanel } from './OrganizationDeletePanel'
import { DataPrivacyForm } from './DataPrivacyForm'
@@ -14,20 +16,43 @@ export const GeneralSettings = () => {
const organizationDeletionEnabled = useIsFeatureEnabled('organizations:delete')
return (
<ScaffoldContainer>
<>
<NoProjectsOnPaidOrgInfo />
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Organization Details</ScaffoldSectionTitle>
<OrganizationDetailsForm />
</ScaffoldSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Organization details</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<OrganizationDetailsForm />
</PageSectionContent>
</PageSection>
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Data Privacy</ScaffoldSectionTitle>
<DataPrivacyForm />
</ScaffoldSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Data privacy</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<DataPrivacyForm />
</PageSectionContent>
</PageSection>
{organizationDeletionEnabled && <OrganizationDeletePanel />}
</ScaffoldContainer>
{organizationDeletionEnabled && (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Danger zone</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<OrganizationDeletePanel />
</PageSectionContent>
</PageSection>
)}
</>
)
}
@@ -1,4 +1,3 @@
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { MANAGED_BY } from 'lib/constants/infrastructure'
@@ -8,28 +7,23 @@ import { DeleteOrganizationButton } from './DeleteOrganizationButton'
export const OrganizationDeletePanel = () => {
const { data: selectedOrganization } = useSelectedOrganizationQuery()
return (
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">Danger Zone</ScaffoldSectionTitle>
{selectedOrganization?.managed_by !== 'vercel-marketplace' ? (
<Admonition
type="destructive"
title="Deleting this organization will also remove its projects"
description="Make sure you have made a backup of your projects if you want to keep your data"
>
<DeleteOrganizationButton />
</Admonition>
) : (
<PartnerManagedResource
managedBy={MANAGED_BY.VERCEL_MARKETPLACE}
resource="Organizations"
cta={{
installationId: selectedOrganization?.partner_id,
path: '/settings',
message: 'Delete organization in Vercel Marketplace',
}}
/>
)}
</ScaffoldSection>
return selectedOrganization?.managed_by !== 'vercel-marketplace' ? (
<Admonition
type="destructive"
title="Deleting this organization will also remove its projects"
description="Make sure you have made a backup of your projects if you want to keep your data"
>
<DeleteOrganizationButton />
</Admonition>
) : (
<PartnerManagedResource
managedBy={MANAGED_BY.VERCEL_MARKETPLACE}
resource="Organizations"
cta={{
installationId: selectedOrganization?.partner_id,
path: '/settings',
message: 'Delete organization in Vercel Marketplace',
}}
/>
)
}
@@ -68,7 +68,7 @@ export const OAuthApps = () => {
return (
<>
<ScaffoldContainer>
<ScaffoldContainer className="px-6 xl:px-10">
<ScaffoldSection isFullWidth className="flex flex-col gap-y-8">
<div className="space-y-4">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-2">
@@ -153,7 +153,7 @@ export const SSOConfig = () => {
}, [ssoConfig, form])
return (
<ScaffoldContainer>
<ScaffoldContainer size="small" className="px-6 xl:px-10">
<ScaffoldSection isFullWidth>
{isLoadingEntitlement || (hasAccessToSso && isLoadingSSOConfig) ? (
<Card>
@@ -105,7 +105,7 @@ export const SecuritySettings = () => {
}
return (
<ScaffoldContainer>
<ScaffoldContainer size="small" className="px-6 xl:px-10">
<ScaffoldSection isFullWidth>
{!isPaidPlan ? (
<UpgradeToPro
@@ -1,13 +1,11 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useState } from 'react'
import { Button, SidePanel } from 'ui'
import { useParams } from 'common'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useState } from 'react'
import { useAppStateSnapshot } from 'state/app-state'
import { Button, SidePanel } from 'ui'
import { Bucket } from './Content/Bucket'
import { EdgeFunction } from './Content/EdgeFunction'
import { EdgeFunctions } from './Content/EdgeFunctions'
@@ -22,6 +20,7 @@ import { UserManagement } from './Content/UserManagement'
import { FirstLevelNav } from './FirstLevelNav'
import LanguageSelector from './LanguageSelector'
import { SecondLevelNav } from './SecondLevelNav'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
/**
* [Joshen] Reminder: when we choose to release this as a main feature
@@ -50,11 +49,8 @@ export const ProjectAPIDocs = () => {
{ projectRef: ref },
{ enabled: snap.showProjectApiDocs && canReadAPIKeys }
)
const { data: settings } = useProjectSettingsV2Query(
{ projectRef: ref },
{ enabled: snap.showProjectApiDocs }
)
const { data: customDomainData } = useCustomDomainsQuery(
const { data: endpoint } = useProjectApiUrl(
{ projectRef: ref },
{ enabled: snap.showProjectApiDocs }
)
@@ -63,12 +59,6 @@ export const ProjectAPIDocs = () => {
const apikey = showKeys
? anonKey?.api_key ?? 'SUPABASE_CLIENT_ANON_KEY'
: 'SUPABASE_CLIENT_ANON_KEY'
const protocol = settings?.app_config?.protocol ?? 'https'
const hostEndpoint = settings?.app_config?.endpoint
const endpoint =
customDomainData?.customDomain?.status === 'active'
? `https://${customDomainData.customDomain?.hostname}`
: `${protocol}://${hostEndpoint ?? ''}`
return (
<SidePanel
@@ -1,16 +1,11 @@
import { UseFormReturn } from 'react-hook-form'
import { useFlag, useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import Panel from 'components/ui/Panel'
import { useDefaultRegionQuery } from 'data/misc/get-default-region-query'
import { useOrganizationAvailableRegionsQuery } from 'data/organizations/organization-available-regions-query'
import type { DesiredInstanceSize } from 'data/projects/new-project.constants'
import { BASE_PATH, PROVIDERS } from 'lib/constants'
import { UseFormReturn } from 'react-hook-form'
import type { CloudProvider } from 'shared-data'
import {
Badge,
cn,
FormField_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
@@ -18,15 +13,23 @@ import {
SelectSeparator_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { CreateProjectForm } from './ProjectCreation.schema'
import { getAvailableRegions } from './ProjectCreation.utils'
import AlertError from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
import Panel from '@/components/ui/Panel'
import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
import type { DesiredInstanceSize } from '@/data/projects/new-project.constants'
import { BASE_PATH, PROVIDERS } from '@/lib/constants'
interface RegionSelectorProps {
form: UseFormReturn<CreateProjectForm>
@@ -38,6 +41,18 @@ interface RegionSelectorProps {
// I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render
// ^ can try again next time
// Maps smart region group codes to the specific-region code prefixes they contain.
// Used to check whether an incident affecting specific regions also affects a smart region selection.
const SMART_REGION_PREFIXES: Record<string, Array<string>> = {
americas: ['us-', 'ca-', 'sa-'],
emea: ['eu-', 'me-', 'af-'],
apac: ['ap-'],
}
function smartRegionMatchesSpecific(smartCode: string, specificCode: string): boolean {
return (SMART_REGION_PREFIXES[smartCode] ?? []).some((prefix) => specificCode.startsWith(prefix))
}
// Map backend region names to user-friendly display names
const getDisplayNameForSmartRegion = (name: string): string => {
if (name === 'APAC') {
@@ -56,6 +71,9 @@ export const RegionSelector = ({
const smartRegionEnabled = useFlag('enableSmartRegion')
const { data: statusData } = useIncidentStatusQuery()
const { incidents = [] } = statusData ?? {}
const { isPending: isLoadingDefaultRegion } = useDefaultRegionQuery(
{ cloudProvider },
{ enabled: !smartRegionEnabled }
@@ -114,150 +132,185 @@ export const RegionSelector = ({
return !!region.name && region.name === field.value
})
const affectingIncidents = incidents.filter((incident) => {
const affectedRegions = incident.cache?.affected_regions ?? []
if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false
// Specific region: direct code match
if (affectedRegions.includes(selectedRegion.code)) return true
// Smart region: match if any affected region falls within the smart group
return affectedRegions.some((specificCode) =>
smartRegionMatchesSpecific(selectedRegion.code, specificCode)
)
})
return (
<FormItemLayout
layout={layout}
label="Region"
description={
<>
<p>Select the region closest to your users for the best performance.</p>
{showNonProdFields && (
<div className="mt-2 text-warning">
<p>Only these regions are supported for local/staging projects:</p>
<ul className="list-disc list-inside mt-1">
<li>East US (North Virginia)</li>
<li>Central EU (Frankfurt)</li>
<li>Southeast Asia (Singapore)</li>
</ul>
</div>
)}
</>
}
>
<Select_Shadcn_
value={field.value}
onValueChange={field.onChange}
disabled={isLoading}
>
<SelectTrigger_Shadcn_ className="[&>:nth-child(1)]:w-full [&>:nth-child(1)]:flex [&>:nth-child(1)]:items-start">
<SelectValue_Shadcn_
placeholder={
isLoading
? 'Loading available regions...'
: 'Select a region for your project..'
}
>
{field.value !== undefined && (
<div className="flex items-center gap-x-3">
{selectedRegion?.code && (
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${selectedRegion.code}.svg`}
/>
)}
<span className="text-foreground">
{selectedRegion?.name
? getDisplayNameForSmartRegion(selectedRegion.name)
: field.value}
</span>
<>
<FormItemLayout
layout={layout}
label="Region"
description={
<>
<p>Select the region closest to your users for the best performance.</p>
{showNonProdFields && (
<div className="mt-2 text-warning">
<p>Only these regions are supported for local/staging projects:</p>
<ul className="list-disc list-inside mt-1">
<li>East US (North Virginia)</li>
<li>Central EU (Frankfurt)</li>
<li>Southeast Asia (Singapore)</li>
</ul>
</div>
)}
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{smartRegionEnabled && (
<>
<SelectGroup_Shadcn_>
<SelectLabel_Shadcn_>General regions</SelectLabel_Shadcn_>
{smartRegions.map((value) => {
return (
<SelectItem_Shadcn_
key={value.code}
value={value.name}
className="w-full [&>:nth-child(2)]:w-full"
>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex items-center gap-x-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${value.code}.svg`}
/>
<span className="text-foreground">
{getDisplayNameForSmartRegion(value.name)}
</>
}
>
<Select_Shadcn_
value={field.value}
onValueChange={field.onChange}
disabled={isLoading}
>
<SelectTrigger_Shadcn_ className="[&>:nth-child(1)]:w-full [&>:nth-child(1)]:flex [&>:nth-child(1)]:items-start">
<SelectValue_Shadcn_
placeholder={
isLoading
? 'Loading available regions...'
: 'Select a region for your project..'
}
>
{field.value !== undefined && (
<div className="flex items-center gap-x-3">
{selectedRegion?.code && (
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${selectedRegion.code}.svg`}
/>
)}
<span className="text-foreground">
{selectedRegion?.name
? getDisplayNameForSmartRegion(selectedRegion.name)
: field.value}
</span>
</div>
)}
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{smartRegionEnabled && (
<>
<SelectGroup_Shadcn_>
<SelectLabel_Shadcn_>General regions</SelectLabel_Shadcn_>
{smartRegions.map((value) => {
return (
<SelectItem_Shadcn_
key={value.code}
value={value.name}
className="w-full [&>:nth-child(2)]:w-full"
>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex items-center gap-x-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${value.code}.svg`}
/>
<span className="text-foreground">
{getDisplayNameForSmartRegion(value.name)}
</span>
</div>
<div>
{recommendedSmartRegions.has(value.code) && (
<Badge variant="success" className="mr-1">
Recommended
</Badge>
)}
</div>
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
<SelectSeparator_Shadcn_ />
</>
)}
<SelectGroup_Shadcn_>
<SelectLabel_Shadcn_>Specific regions</SelectLabel_Shadcn_>
{regionOptions.map((value) => {
return (
<SelectItem_Shadcn_
key={value.code}
value={value.name}
className={cn(
'w-full [&>:nth-child(2)]:w-full',
value.status !== undefined && '!pointer-events-auto'
)}
disabled={value.status !== undefined}
>
<div className="flex flex-row items-center justify-between w-full gap-x-2">
<div className="flex items-center gap-x-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${value.code}.svg`}
/>
<div className="flex items-center gap-x-2">
<span className="text-foreground">{value.name}</span>
<span className="text-xs text-foreground-lighter font-mono">
{value.code}
</span>
</div>
</div>
<div>
{recommendedSmartRegions.has(value.code) && (
<Badge variant="success" className="mr-1">
Recommended
{recommendedSpecificRegions.has(value.code) && (
<Badge variant="success" className="mr-1">
Recommended
</Badge>
)}
{value.status !== undefined && value.status === 'capacity' && (
<Tooltip>
<TooltipTrigger>
<Badge variant="warning" className="mr-1">
Unavailable
</Badge>
)}
</div>
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
<SelectSeparator_Shadcn_ />
</>
)}
<SelectGroup_Shadcn_>
<SelectLabel_Shadcn_>Specific regions</SelectLabel_Shadcn_>
{regionOptions.map((value) => {
return (
<SelectItem_Shadcn_
key={value.code}
value={value.name}
className={cn(
'w-full [&>:nth-child(2)]:w-full',
value.status !== undefined && '!pointer-events-auto'
)}
disabled={value.status !== undefined}
>
<div className="flex flex-row items-center justify-between w-full gap-x-2">
<div className="flex items-center gap-x-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${value.code}.svg`}
/>
<div className="flex items-center gap-x-2">
<span className="text-foreground">{value.name}</span>
<span className="text-xs text-foreground-lighter font-mono">
{value.code}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
Temporarily unavailable due to this region being at capacity.
</TooltipContent>
</Tooltip>
)}
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
{recommendedSpecificRegions.has(value.code) && (
<Badge variant="success" className="mr-1">
Recommended
</Badge>
)}
{value.status !== undefined && value.status === 'capacity' && (
<Tooltip>
<TooltipTrigger>
<Badge variant="warning" className="mr-1">
Unavailable
</Badge>
</TooltipTrigger>
<TooltipContent>
Temporarily unavailable due to this region being at capacity.
</TooltipContent>
</Tooltip>
)}
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
{affectingIncidents.length > 0 && (
<FormItemLayout layout="horizontal">
<Admonition
type="warning"
title="Incident in progress for this region"
description={
<>
We're currently investigating an issue that may impact projects in this
region. Follow updates on{' '}
<InlineLink href="https://status.supabase.com">
status.supabase.com
</InlineLink>
.
</>
}
className="mt-3"
/>
</FormItemLayout>
)}
</>
)
}}
/>
@@ -15,14 +15,14 @@ import {
import { Input } from 'ui-patterns/DataInputs/Input'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { getApiEndpoint } from '@/components/interfaces/Integrations/DataApi/DataApi.utils'
import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useLoadBalancersQuery } from '@/data/read-replicas/load-balancers-query'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
import { getApiEndpoint } from '@/components/interfaces/Integrations/DataApi/DataApi.utils'
export const DataApiProjectUrlCard = () => {
const { isPending: isLoading } = useSelectedProjectQuery()
@@ -31,7 +31,7 @@ export const DataApiProjectUrlCard = () => {
const [querySource, setQuerySource] = useQueryState('source', parseAsString)
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const { data: resolvedEndpoint } = useProjectApiUrl({ projectRef })
const {
data: databases,
isError,
@@ -55,7 +55,7 @@ export const DataApiProjectUrlCard = () => {
const endpoint = getApiEndpoint({
selectedDatabaseId: state.selectedDatabaseId,
projectRef,
customDomainData,
resolvedEndpoint,
loadBalancers,
selectedDatabase,
})
@@ -418,7 +418,9 @@ export const Addons = () => {
!isProjectActive || projectUpdateDisabled || !(canUpdateIPv4 || ipv4)
}
>
Change dedicated IPv4 address
{!!ipv4
? 'Toggle dedicated IPv4 address'
: 'Enable dedicated IPv4 address'}
</Button>
</ProjectUpdateDisabledTooltip>
</div>
@@ -528,7 +530,7 @@ export const Addons = () => {
},
}}
>
Change point in time recovery
Enable point in time recovery
</ButtonTooltip>
) : (
<ProjectUpdateDisabledTooltip
@@ -546,7 +548,7 @@ export const Addons = () => {
hasHipaaAddon
}
>
Change point in time recovery
{!!pitr ? 'Change recovery duration' : 'Enable point in time recovery'}
</Button>
</ProjectUpdateDisabledTooltip>
)}
@@ -608,19 +610,28 @@ export const Addons = () => {
? 'Custom domain is enabled'
: 'Custom domain is not enabled'}
</p>
<ProjectUpdateDisabledTooltip
projectUpdateDisabled={projectUpdateDisabled}
projectNotActive={!isProjectActive}
>
<Button
type="default"
className="mt-2 pointer-events-auto"
onClick={() => setPanel('customDomain')}
disabled={!isProjectActive || projectUpdateDisabled}
<div className="mt-2 flex items-center gap-x-2">
<ProjectUpdateDisabledTooltip
projectUpdateDisabled={projectUpdateDisabled}
projectNotActive={!isProjectActive}
>
Change custom domain
</Button>
</ProjectUpdateDisabledTooltip>
<Button
type="default"
className="pointer-events-auto"
onClick={() => setPanel('customDomain')}
disabled={!isProjectActive || projectUpdateDisabled}
>
{!!customDomain ? 'Toggle custom domain' : 'Enable custom domain'}
</Button>
</ProjectUpdateDisabledTooltip>
{!!customDomain && (
<Button asChild type="outline">
<Link href={`/project/${projectRef}/settings/general#custom-domains`}>
Edit custom domain
</Link>
</Button>
)}
</div>
</div>
</div>
</ScaffoldSectionContent>
@@ -1,6 +1,3 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { DocsButton } from 'components/ui/DocsButton'
import Panel from 'components/ui/Panel'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -9,16 +6,18 @@ import { useCustomDomainActivateMutation } from 'data/custom-domains/custom-doma
import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation'
import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query'
import { DOCS_URL } from 'lib/constants'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
export type CustomDomainActivateProps = {
projectRef?: string
customDomain: CustomDomainResponse
}
const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivateProps) => {
export const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivateProps) => {
const [isActivateConfirmModalVisible, setIsActivateConfirmModalVisible] = useState(false)
const { data: settings } = useProjectSettingsV2Query({ projectRef })
@@ -134,5 +133,3 @@ const CustomDomainActivate = ({ projectRef, customDomain }: CustomDomainActivate
</>
)
}
export default CustomDomainActivate
@@ -1,5 +1,3 @@
import { AlertCircle } from 'lucide-react'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import { SupportLink } from 'components/interfaces/Support/SupportLink'
@@ -11,6 +9,7 @@ import {
} from 'data/custom-domains/custom-domains-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { AlertCircle } from 'lucide-react'
import { Card, CardContent } from 'ui'
import {
PageSection,
@@ -20,11 +19,12 @@ import {
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import CustomDomainActivate from './CustomDomainActivate'
import CustomDomainDelete from './CustomDomainDelete'
import CustomDomainVerify from './CustomDomainVerify'
import CustomDomainsConfigureHostname from './CustomDomainsConfigureHostname'
import CustomDomainsShimmerLoader from './CustomDomainsShimmerLoader'
import { CustomDomainActivate } from './CustomDomainActivate'
import { CustomDomainDelete } from './CustomDomainDelete'
import { CustomDomainsConfigureHostname } from './CustomDomainsConfigureHostname'
import { CustomDomainsShimmerLoader } from './CustomDomainsShimmerLoader'
import { CustomDomainVerify } from './CustomDomainVerify'
export const CustomDomainConfig = () => {
const { ref } = useParams()
@@ -1,12 +1,11 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { DocsButton } from 'components/ui/DocsButton'
import Panel from 'components/ui/Panel'
import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domains-delete-mutation'
import type { CustomDomainResponse } from 'data/custom-domains/custom-domains-query'
import { DOCS_URL } from 'lib/constants'
import { Trash } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -15,7 +14,7 @@ export type CustomDomainDeleteProps = {
customDomain: CustomDomainResponse
}
const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProps) => {
export const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProps) => {
const [isDeleteConfirmModalVisible, setIsDeleteConfirmModalVisible] = useState(false)
const { mutate: deleteCustomDomain, isPending: isDeletingCustomDomain } =
useCustomDomainDeleteMutation({
@@ -85,5 +84,3 @@ const CustomDomainDelete = ({ projectRef, customDomain }: CustomDomainDeleteProp
</>
)
}
export default CustomDomainDelete
@@ -1,6 +1,3 @@
import { AlertCircle, RefreshCw } from 'lucide-react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { DocsButton } from 'components/ui/DocsButton'
import { InlineLink } from 'components/ui/InlineLink'
@@ -10,19 +7,22 @@ import { useCustomDomainDeleteMutation } from 'data/custom-domains/custom-domain
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useCustomDomainReverifyQuery } from 'data/custom-domains/custom-domains-reverify-query'
import { DOCS_URL } from 'lib/constants'
import { AlertCircle, RefreshCw } from 'lucide-react'
import { useEffect } from 'react'
import { toast } from 'sonner'
import {
Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
WarningIcon,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import DNSRecord from './DNSRecord'
import { DNSTableHeaders } from './DNSTableHeaders'
const CustomDomainVerify = () => {
export const CustomDomainVerify = () => {
const { ref: projectRef } = useParams()
const { data: settings } = useProjectSettingsV2Query({ projectRef })
@@ -208,5 +208,3 @@ const CustomDomainVerify = () => {
</>
)
}
export default CustomDomainVerify
@@ -1,8 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { useParams } from 'common'
import { DocsButton } from 'components/ui/DocsButton'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
@@ -11,6 +8,7 @@ import { useCustomDomainCreateMutation } from 'data/custom-domains/custom-domain
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DOCS_URL } from 'lib/constants'
import { useForm } from 'react-hook-form'
import {
Button,
Card,
@@ -18,19 +16,19 @@ import {
CardFooter,
CardHeader,
CardTitle,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
FormMessage_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'
const schema = z.object({
domain: z.string().trim().min(1, 'A value for your custom domain is required'),
})
const CustomDomainsConfigureHostname = () => {
export const CustomDomainsConfigureHostname = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
@@ -102,7 +100,6 @@ const CustomDomainsConfigureHostname = () => {
autoComplete="off"
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormItemLayout>
)}
/>
@@ -154,5 +151,3 @@ const CustomDomainsConfigureHostname = () => {
</Form_Shadcn_>
)
}
export default CustomDomainsConfigureHostname
@@ -1,4 +1,4 @@
const CustomDomainsShimmerLoader = () => {
export const CustomDomainsShimmerLoader = () => {
return (
<div className="grid grid-cols-12 gap-6 px-8 py-8">
<div className="col-span-12 lg:col-span-5">
@@ -11,5 +11,3 @@ const CustomDomainsShimmerLoader = () => {
</div>
)
}
export default CustomDomainsShimmerLoader
+10 -6
View File
@@ -373,6 +373,14 @@ const OrganizationLinks = () => {
const showBilling = useIsFeatureEnabled('billing:all')
const activeRoute = router.pathname.split('/')[3]
const organizationSettingsRoutes = new Set([
'general',
'security',
'sso',
'apps',
'audit',
'documents',
])
const navMenuItems = [
{
@@ -410,7 +418,7 @@ const OrganizationLinks = () => {
]
: []),
{
label: 'Organization settings',
label: 'Organization Settings',
href: `/org/${organizationSlug}/general`,
key: 'settings',
icon: <Settings size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
@@ -429,11 +437,7 @@ const OrganizationLinks = () => {
i === 0
? activeRoute === undefined
: item.key === 'settings'
? router.pathname.includes('/general') ||
router.pathname.includes('/apps') ||
router.pathname.includes('/audit') ||
router.pathname.includes('/documents') ||
router.pathname.includes('/security')
? organizationSettingsRoutes.has(activeRoute ?? '')
: activeRoute === item.key
}
route={{
@@ -1,22 +1,18 @@
import { useCallback } from 'react'
import { toast } from 'sonner'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import { copyToClipboard } from 'ui'
import { URL_EXPIRY_DURATION } from '../Storage.constants'
import { getPathAlongOpenedFolders } from './StorageExplorer.utils'
import { fetchFileUrl } from './useFetchFileUrlQuery'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
export const useCopyUrl = () => {
const { projectRef, selectedBucket, openedFolders } = useStorageExplorerStateSnapshot()
const { data: customDomainData } = useCustomDomainsQuery({ projectRef: projectRef })
const { data: settings } = useProjectSettingsV2Query({ projectRef: projectRef })
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const apiUrl = `${protocol}://${endpoint ?? '-'}`
const { hostEndpoint, customEndpoint } = useProjectApiUrl({ projectRef })
const isCustomDomainActive = !!customEndpoint
const getFileUrl = useCallback(
(fileName: string, expiresIn?: URL_EXPIRY_DURATION) => {
@@ -37,8 +33,8 @@ export const useCopyUrl = () => {
const onCopyUrl = useCallback(
(name: string, expiresIn?: URL_EXPIRY_DURATION) => {
const formattedUrl = getFileUrl(name, expiresIn).then((url) => {
return customDomainData?.customDomain?.status === 'active'
? url.replace(apiUrl, `https://${customDomainData.customDomain.hostname}`)
return isCustomDomainActive && hostEndpoint
? url.replace(hostEndpoint, customEndpoint)
: url
})
@@ -46,12 +42,7 @@ export const useCopyUrl = () => {
toast.success(`Copied URL for ${name} to clipboard.`)
})
},
[
apiUrl,
customDomainData?.customDomain?.hostname,
customDomainData?.customDomain?.status,
getFileUrl,
]
[customEndpoint, getFileUrl, hostEndpoint, isCustomDomainActive]
)
return { onCopyUrl }
@@ -1,3 +1,8 @@
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useIsSchemaExposed } from 'hooks/misc/useIsSchemaExposed'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Settings } from 'lucide-react'
import Link from 'next/link'
import {
@@ -10,7 +15,19 @@ import {
type SetStateAction,
} from 'react'
import { usePreviousDistinct } from 'react-use'
import { Button, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Switch } from 'ui'
import { Admonition } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useTableApiAccessQuery } from '@/data/privileges/table-api-access-query'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
import {
@@ -25,23 +42,6 @@ import {
} from '@/lib/data-api-types'
import type { DeepReadonly, Prettify } from '@/lib/type-helpers'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useLoadBalancersQuery } from 'data/read-replicas/load-balancers-query'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { useIsSchemaExposed } from 'hooks/misc/useIsSchemaExposed'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Button, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Switch } from 'ui'
import { Admonition } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import {
MultiSelector,
MultiSelectorContent,
MultiSelectorItem,
MultiSelectorList,
MultiSelectorTrigger,
} from 'ui-patterns/multi-select'
const ROLE_LABELS: Record<ApiAccessRole, string> = {
anon: 'Anonymous (anon)',
@@ -390,6 +390,7 @@ export const ApiAccessToggle = ({
</div>
</div>
</div>
<SchemaExposureOptions
projectRef={projectRef}
schemaName={schemaName}
@@ -422,14 +423,13 @@ const SchemaExposureOptions = ({
}): ReactNode => {
const { selectedDatabaseId } = useDatabaseSelectorStateSnapshot()
const { data: customDomainData } = useCustomDomainsQuery({ projectRef })
const { data: endpoint } = useProjectApiUrl({ projectRef })
const { data: loadBalancers } = useLoadBalancersQuery({ projectRef })
const { data: databases } = useReadReplicasQuery({ projectRef })
const apiEndpoint = useMemo(() => {
const isCustomDomainActive = customDomainData?.customDomain?.status === 'active'
if (isCustomDomainActive && selectedDatabaseId === projectRef) {
return `https://${customDomainData.customDomain.hostname}`
if (selectedDatabaseId === projectRef) {
return endpoint
}
const loadBalancerSelected = selectedDatabaseId === 'load-balancer'
@@ -439,14 +439,8 @@ const SchemaExposureOptions = ({
const selectedDatabase = databases?.find((db) => db.identifier === selectedDatabaseId)
return selectedDatabase?.restUrl
}, [
projectRef,
databases,
selectedDatabaseId,
customDomainData?.customDomain?.status,
customDomainData?.customDomain?.hostname,
loadBalancers,
])
}, [selectedDatabaseId, projectRef, databases, endpoint, loadBalancers])
const apiBaseUrl = useMemo(() => {
if (!apiEndpoint) return undefined
return apiEndpoint.endsWith('/') ? apiEndpoint.slice(0, -1) : apiEndpoint
@@ -56,14 +56,14 @@ describe('shouldShowBanner', () => {
).toBe(true)
})
it('shows when affects_project_creation is true even with a region restriction', () => {
it('does not show when affects_project_creation is true but there is a region restriction', () => {
expect(
shouldShowBanner({
incidents: [usEast1AndCreation],
hasProjects: false,
userRegions: new Set(),
})
).toBe(true)
).toBe(false)
})
})
@@ -221,7 +221,7 @@ describe('shouldShowBanner', () => {
).toBe(false)
})
it('still shows for affects_project_creation with no projects even when regions are unknown', () => {
it('does not show for affects_project_creation with no projects when there is a region restriction, even when regions are unknown', () => {
expect(
shouldShowBanner({
incidents: [usEast1AndCreation],
@@ -229,7 +229,7 @@ describe('shouldShowBanner', () => {
userRegions: new Set(),
hasUnknownRegions: true,
})
).toBe(true)
).toBe(false)
})
})
})
@@ -30,7 +30,8 @@ export function shouldShowBanner({
const affectsProjectCreation = incident.cache?.affects_project_creation ?? false
// Users with no projects only see the banner if the incident affects project creation
if (!hasProjects) return affectsProjectCreation
// and has no specific region targeting (inline notice in RegionSelector handles region-specific incidents)
if (!hasProjects) return affectsProjectCreation && affectedRegions.length === 0
// User has projects: if no region restriction, always show
if (affectedRegions.length === 0) return true
@@ -19,7 +19,8 @@ export function useStatusPageBannerVisibility(): StatusPageBannerData | null {
useFlag('ongoingIncident') || process.env.NEXT_PUBLIC_ONGOING_INCIDENT === 'true'
const { data: allStatusPageEvents } = useIncidentStatusQuery()
const { incidents = [] } = allStatusPageEvents ?? {}
const { incidents: allIncidents = [] } = allStatusPageEvents ?? {}
const incidents = allIncidents.filter((i) => i.impact !== 'none')
const hasActiveIncidents = incidents.length > 0
@@ -1,60 +1,21 @@
import { ProductMenu } from 'components/ui/ProductMenu'
import { withAuth } from 'hooks/misc/withAuth'
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
import { useIsColumnLevelPrivilegesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication/useIsETLPrivateAlpha'
import { ProductMenu } from 'components/ui/ProductMenu'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { withAuth } from 'hooks/misc/withAuth'
import { ProjectLayout } from '../ProjectLayout'
import { generateDatabaseMenu } from './DatabaseMenu.utils'
import { useGenerateDatabaseMenu } from './DatabaseMenu.utils'
export interface DatabaseLayoutProps {
title?: string
}
const DatabaseProductMenu = () => {
const { data: project } = useSelectedProjectQuery()
const router = useRouter()
const page = router.pathname.split('/')[4]
const menu = useGenerateDatabaseMenu()
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
const enablePgReplicate = useIsETLPrivateAlpha()
const {
databaseReplication: showPgReplicate,
databaseRoles: showRoles,
integrationsWrappers: showWrappers,
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
return (
<>
<ProductMenu
page={page}
menu={generateDatabaseMenu(project, {
pgNetExtensionExists,
pitrEnabled,
columnLevelPrivileges,
showPgReplicate,
enablePgReplicate,
showRoles,
showWrappers,
})}
/>
</>
)
return <ProductMenu page={page} menu={menu} />
}
const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) => {
@@ -1,163 +1,104 @@
import { useParams } from 'common'
import type {
ProductMenuGroup,
ProductMenuGroupItem,
} from 'components/ui/ProductMenu/ProductMenu.types'
import { IS_PLATFORM } from 'lib/constants'
import { ArrowUpRight } from 'lucide-react'
import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import type { Project } from 'data/projects/project-detail-query'
import { IS_PLATFORM } from 'lib/constants'
import { useIsColumnLevelPrivilegesEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useIsETLPrivateAlpha } from '@/components/interfaces/Database/Replication/useIsETLPrivateAlpha'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const ExternalLinkIcon = <ArrowUpRight strokeWidth={1} className="h-4 w-4" />
export const useGenerateDatabaseMenu = (): ProductMenuGroup[] => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
export const generateDatabaseMenu = (
project?: Project,
flags?: {
pgNetExtensionExists: boolean
pitrEnabled: boolean
columnLevelPrivileges: boolean
showPgReplicate: boolean
enablePgReplicate: boolean
showRoles: boolean
showWrappers: boolean
}
): ProductMenuGroup[] => {
const ref = project?.ref ?? 'default'
const {
pgNetExtensionExists,
pitrEnabled,
columnLevelPrivileges,
showPgReplicate,
enablePgReplicate,
showRoles,
showWrappers,
} = flags || {}
databaseReplication: showPgReplicate,
databaseRoles: showRoles,
integrationsWrappers: showWrappers,
} = useIsFeatureEnabled(['database:replication', 'database:roles', 'integrations:wrappers'])
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
const pgNetExtensionExists = (data ?? []).some((ext) => ext.name === 'pg_net')
const pitrEnabled = addons?.selected_addons.some((addon) => addon.type === 'pitr') ?? false
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
const enablePgReplicate = useIsETLPrivateAlpha()
const getDatabaseURL = (path: string) => `/project/${ref}/database/${path}`
return [
{
title: 'Database Management',
items: [
{
name: 'Schema Visualizer',
key: 'schemas',
url: `/project/${ref}/database/schemas`,
items: [],
},
{ name: 'Tables', key: 'tables', url: `/project/${ref}/database/tables`, items: [] },
{
name: 'Functions',
key: 'functions',
url: `/project/${ref}/database/functions`,
items: [],
},
{
name: 'Triggers',
key: 'triggers',
url: `/project/${ref}/database/triggers/data`,
items: [],
},
{
name: 'Enumerated Types',
key: 'types',
url: `/project/${ref}/database/types`,
items: [],
},
{
name: 'Extensions',
key: 'extensions',
url: `/project/${ref}/database/extensions`,
items: [],
},
{
name: 'Indexes',
key: 'indexes',
url: `/project/${ref}/database/indexes`,
items: [],
},
{
name: 'Publications',
key: 'publications',
url: `/project/${ref}/database/publications`,
items: [],
},
{ name: 'Schema Visualizer', key: 'schemas', url: getDatabaseURL('schemas') },
{ name: 'Tables', key: 'tables', url: getDatabaseURL('tables') },
{ name: 'Functions', key: 'functions', url: getDatabaseURL('functions') },
{ name: 'Triggers', key: 'triggers', url: getDatabaseURL('triggers/data') },
{ name: 'Enumerated Types', key: 'types', url: getDatabaseURL('types') },
{ name: 'Extensions', key: 'extensions', url: getDatabaseURL('extensions') },
{ name: 'Indexes', key: 'indexes', url: getDatabaseURL('indexes') },
{ name: 'Publications', key: 'publications', url: getDatabaseURL('publications') },
],
},
{
title: 'Configuration',
items: [
...(showRoles
? [{ name: 'Roles', key: 'roles', url: `/project/${ref}/database/roles`, items: [] }]
: []),
...(columnLevelPrivileges
? [
{
name: 'Column Privileges',
key: 'column-privileges',
url: `/project/${ref}/database/column-privileges`,
items: [],
},
]
: []),
showRoles && { name: 'Roles', key: 'roles', url: getDatabaseURL('roles') },
columnLevelPrivileges && {
name: 'Column Privileges',
key: 'column-privileges',
url: getDatabaseURL('column-privileges'),
},
{
name: 'Policies',
key: 'policies',
url: `/project/${ref}/auth/policies`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
rightIcon: ExternalLinkIcon,
},
{ name: 'Settings', key: 'settings', url: `/project/${ref}/database/settings`, items: [] },
],
{ name: 'Settings', key: 'settings', url: getDatabaseURL('settings') },
].filter(Boolean) as ProductMenuGroupItem[],
},
{
title: 'Platform',
items: [
...(IS_PLATFORM && showPgReplicate
? [
{
name: 'Replication',
key: 'replication',
url: `/project/${ref}/database/replication`,
label: enablePgReplicate ? 'New' : undefined,
items: [],
},
]
: []),
...(IS_PLATFORM
? [
{
name: 'Backups',
key: 'backups',
url: pitrEnabled
? `/project/${ref}/database/backups/pitr`
: `/project/${ref}/database/backups/scheduled`,
items: [],
},
]
: []),
{
name: 'Migrations',
key: 'migrations',
url: `/project/${ref}/database/migrations`,
items: [],
IS_PLATFORM &&
showPgReplicate && {
name: 'Replication',
key: 'replication',
url: getDatabaseURL('replication'),
label: enablePgReplicate ? 'New' : undefined,
},
IS_PLATFORM && {
name: 'Backups',
key: 'backups',
url: pitrEnabled ? getDatabaseURL('backups/pitr') : getDatabaseURL('backups/scheduled'),
},
...(showWrappers
? [
{
name: 'Wrappers',
key: 'wrappers',
url: `/project/${ref}/integrations?category=wrapper`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
]
: []),
...(!!pgNetExtensionExists
? [
{
name: 'Webhooks',
key: 'hooks',
url: `/project/${ref}/integrations/webhooks/overview`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
},
]
: []),
],
{ name: 'Migrations', key: 'migrations', url: getDatabaseURL('migrations') },
showWrappers && {
name: 'Wrappers',
key: 'wrappers',
url: `/project/${ref}/integrations?category=wrapper`,
rightIcon: ExternalLinkIcon,
},
pgNetExtensionExists && {
name: 'Webhooks',
key: 'hooks',
url: `/project/${ref}/integrations/webhooks/overview`,
rightIcon: ExternalLinkIcon,
},
].filter(Boolean) as ProductMenuGroupItem[],
},
{
title: 'Tools',
@@ -166,22 +107,19 @@ export const generateDatabaseMenu = (
name: 'Security Advisor',
key: 'security-advisor',
url: `/project/${ref}/advisors/security`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
rightIcon: ExternalLinkIcon,
},
{
name: 'Performance Advisor',
key: 'performance-advisor',
url: `/project/${ref}/advisors/performance`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
rightIcon: ExternalLinkIcon,
},
{
name: 'Query Performance',
key: 'query-performance',
url: `/project/${ref}/observability/query-performance`,
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
items: [],
rightIcon: ExternalLinkIcon,
},
],
},
@@ -1,6 +1,4 @@
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
import { generateAuthMenu } from 'components/layouts/AuthLayout/AuthLayout.utils'
import { generateDatabaseMenu } from 'components/layouts/DatabaseLayout/DatabaseMenu.utils'
import { generateSettingsMenu } from 'components/layouts/ProjectSettingsLayout/SettingsMenu.utils'
import type { Route } from 'components/ui/ui.types'
import { EditorIndexPageLink } from 'data/prefetchers/project.$ref.editor'
@@ -54,9 +52,6 @@ export const generateProductRoutes = (
const realtimeEnabled = features?.realtime ?? true
const authOverviewPageEnabled = features?.authOverviewPage ?? false
const databaseMenu = generateDatabaseMenu(project)
const authMenu = generateAuthMenu(ref as string)
return [
{
key: 'database',
@@ -70,7 +65,6 @@ export const generateProductRoutes = (
: isProjectActive
? `/project/${ref}/database/schemas`
: `/project/${ref}/database/backups/scheduled`),
items: databaseMenu,
},
...(authEnabled
? [
@@ -86,7 +80,6 @@ export const generateProductRoutes = (
: authOverviewPageEnabled
? `/project/${ref}/auth/overview`
: `/project/${ref}/auth/users`),
items: authMenu,
},
]
: []),
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest'
import {
generateOrganizationSettingsSections,
getOrganizationSettingsDocumentTitle,
normalizeOrganizationSettingsPath,
} from './OrganizationSettingsLayout'
describe('OrganizationSettingsLayout helpers', () => {
it('returns expected organization settings sections and links', () => {
const sections = generateOrganizationSettingsSections({
slug: 'my-org',
currentPath: '/org/my-org/general',
showSecuritySettings: true,
showSsoSettings: true,
showLegalDocuments: true,
})
expect(sections.map((section) => section.heading)).toEqual([
'Configuration',
'Connections',
'Compliance',
])
expect(sections.flatMap((section) => section.links.map((item) => item.label))).toEqual([
'General',
'Security',
'SSO',
'OAuth Apps',
'Audit Logs',
'Legal Documents',
])
expect(
sections.flatMap((section) => section.links).find((item) => item.label === 'General')
?.isActive
).toBe(true)
})
it('hides feature-flagged items when flags are disabled', () => {
const sections = generateOrganizationSettingsSections({
slug: 'my-org',
currentPath: '/org/my-org/general',
showSecuritySettings: false,
showSsoSettings: false,
showLegalDocuments: false,
})
expect(sections.map((section) => section.heading)).toEqual([
'Configuration',
'Connections',
'Compliance',
])
expect(sections.flatMap((section) => section.links.map((item) => item.label))).toEqual([
'General',
'OAuth Apps',
'Audit Logs',
])
})
it('normalizes hash paths for active state checks', () => {
const currentPath = normalizeOrganizationSettingsPath('/org/my-org/security#sso')
const sections = generateOrganizationSettingsSections({
slug: 'my-org',
currentPath,
showSecuritySettings: true,
showSsoSettings: true,
showLegalDocuments: true,
})
expect(
sections.flatMap((section) => section.links).find((item) => item.label === 'Security')
?.isActive
).toBe(true)
})
it('uses settings as default document title when page title is not provided', () => {
expect(getOrganizationSettingsDocumentTitle(undefined, 'Supabase')).toBe('Settings | Supabase')
})
it('uses page title for document title when provided', () => {
expect(getOrganizationSettingsDocumentTitle('General', 'Supabase')).toBe('General | Supabase')
})
})
@@ -1,17 +1,127 @@
import Link from 'next/link'
import Head from 'next/head'
import { PropsWithChildren } from 'react'
import { useParams } from 'common'
import type { SidebarSection } from 'components/layouts/AccountLayout/AccountLayout.types'
import { WithSidebar } from 'components/layouts/AccountLayout/WithSidebar'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useCurrentPath } from 'hooks/misc/useCurrentPath'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { NavMenu, NavMenuItem } from 'ui'
import { ScaffoldContainerLegacy, ScaffoldTitle } from '../Scaffold'
function OrganizationSettingsLayout({ children }: PropsWithChildren) {
interface OrganizationSettingsSectionsProps {
slug?: string
currentPath: string
showSecuritySettings?: boolean
showSsoSettings?: boolean
showLegalDocuments?: boolean
}
interface OrganizationSettingsLayoutProps {
pageTitle?: string
}
export const normalizeOrganizationSettingsPath = (path: string) => path.split('#')[0]
export const getOrganizationSettingsPageTitle = (pageTitle?: string) => pageTitle ?? 'Settings'
export const getOrganizationSettingsDocumentTitle = (
pageTitle: string | undefined,
title: string
) => `${getOrganizationSettingsPageTitle(pageTitle)} | ${title}`
export const generateOrganizationSettingsSections = ({
slug,
currentPath,
showSecuritySettings = true,
showSsoSettings = true,
showLegalDocuments = true,
}: OrganizationSettingsSectionsProps): SidebarSection[] => {
const configurationLinks = [
{
key: 'general',
label: 'General',
href: `/org/${slug}/general`,
},
...(showSecuritySettings
? [
{
key: 'security',
label: 'Security',
href: `/org/${slug}/security`,
},
]
: []),
...(showSsoSettings
? [
{
key: 'sso',
label: 'SSO',
href: `/org/${slug}/sso`,
},
]
: []),
]
const connectionsLinks = [
{
key: 'apps',
label: 'OAuth Apps',
href: `/org/${slug}/apps`,
},
]
const complianceLinks = [
{
key: 'audit',
label: 'Audit Logs',
href: `/org/${slug}/audit`,
},
...(showLegalDocuments
? [
{
key: 'documents',
label: 'Legal Documents',
href: `/org/${slug}/documents`,
},
]
: []),
]
return [
{
key: 'configuration',
heading: 'Configuration',
links: configurationLinks.map((item) => ({
...item,
isActive: currentPath === item.href,
})),
},
{
key: 'connections',
heading: 'Connections',
links: connectionsLinks.map((item) => ({
...item,
isActive: currentPath === item.href,
})),
},
{
key: 'compliance',
heading: 'Compliance',
links: complianceLinks.map((item) => ({
...item,
isActive: currentPath === item.href,
})),
},
]
}
function OrganizationSettingsLayout({
children,
pageTitle,
}: PropsWithChildren<OrganizationSettingsLayoutProps>) {
const { slug } = useParams()
// Get the path without any hash values
const fullCurrentPath = useCurrentPath()
const [currentPath] = fullCurrentPath.split('#')
const currentPath = normalizeOrganizationSettingsPath(fullCurrentPath)
const { appTitle } = useCustomContent(['app:title'])
const titleSuffix = appTitle || 'Supabase'
const {
organizationShowSsoSettings: showSsoSettings,
@@ -23,59 +133,32 @@ function OrganizationSettingsLayout({ children }: PropsWithChildren) {
'organization:show_legal_documents',
])
const navMenuItems = [
{
label: 'General',
href: `/org/${slug}/general`,
},
...(showSecuritySettings
? [
{
label: 'Security',
href: `/org/${slug}/security`,
},
]
: []),
{
label: 'OAuth Apps',
href: `/org/${slug}/apps`,
},
...(showSsoSettings
? [
{
label: 'SSO',
href: `/org/${slug}/sso`,
},
]
: []),
{
label: 'Audit Logs',
href: `/org/${slug}/audit`,
},
...(showLegalDocuments
? [
{
label: 'Legal Documents',
href: `/org/${slug}/documents`,
},
]
: []),
]
const sections = generateOrganizationSettingsSections({
slug,
currentPath,
showSecuritySettings,
showSsoSettings,
showLegalDocuments,
})
return (
<>
<ScaffoldContainerLegacy className="mb-0">
<ScaffoldTitle>Organization Settings</ScaffoldTitle>
<NavMenu className="overflow-x-auto" aria-label="Organization menu navigation">
{(navMenuItems.filter(Boolean) as { label: string; href: string }[]).map((item) => (
<NavMenuItem key={item.label} active={currentPath === item.href}>
<Link href={item.href}>{item.label}</Link>
</NavMenuItem>
))}
</NavMenu>
</ScaffoldContainerLegacy>
<div className="h-full w-full overflow-y-auto">{children}</div>
<Head>
<title>{getOrganizationSettingsDocumentTitle(pageTitle, titleSuffix)}</title>
<meta name="description" content="Supabase Studio" />
</Head>
<WithSidebar
title="Organization Settings"
breadcrumbs={[]}
sections={sections}
header={
<div className="border-default flex min-h-[var(--header-height)] items-center border-b px-6">
<h4 className="text-lg">Settings</h4>
</div>
}
>
{children}
</WithSidebar>
</>
)
}
@@ -236,23 +236,9 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
{isPauseStatusSuccess && !isRestoreDisabled && (
<CardFooter className="flex justify-end items-center gap-x-2">
{isFreePlan ? (
<Button asChild type="default">
<Link
href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=projectPausedStateRestore`}
>
Upgrade to Pro
</Link>
</Button>
) : (
<Button asChild type="default">
<Link href={`/project/${ref}/settings/general`}>View project settings</Link>
</Button>
)}
<ButtonTooltip
size="tiny"
type="primary"
type="default"
disabled={!canResumeProject}
onClick={onSelectRestore}
tooltip={{
@@ -266,6 +252,20 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
>
Resume project
</ButtonTooltip>
{isFreePlan ? (
<Button asChild type="primary">
<Link
href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=projectPausedStateRestore`}
>
Upgrade to Pro
</Link>
</Button>
) : (
<Button asChild type="default">
<Link href={`/project/${ref}/settings/general`}>View project settings</Link>
</Button>
)}
</CardFooter>
)}
@@ -22,7 +22,7 @@ export const generateSettingsMenu = (
if (!IS_PLATFORM) {
return [
{
title: 'Project Settings',
title: 'Configuration',
items: [
{
name: `Log Drains`,
@@ -42,7 +42,7 @@ export const generateSettingsMenu = (
return [
{
title: 'Project Settings',
title: 'Configuration',
items: [
{
name: 'General',
@@ -106,7 +106,7 @@ export const generateSettingsMenu = (
],
},
{
title: 'Configuration',
title: 'Integrations',
items: [
{
name: 'Data API',
@@ -6,6 +6,7 @@ import { PropsWithChildren, useEffect } from 'react'
import { ProjectLayoutWithAuth } from '../ProjectLayout'
import { SaveQueueActionBar } from '@/components/grid/components/footer/operations/SaveQueueActionBar'
import { useIsTableFilterBarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { BannerTableEditorFilter } from '@/components/ui/BannerStack/Banners/BannerTableEditorFilter'
import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
@@ -15,6 +16,7 @@ const TABLE_EDITOR_NEW_FILTER_BANNER_ID = 'table-editor-new-filter-banner'
export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
const { ref } = useParams()
const { addBanner, dismissBanner } = useBannerStack()
const isTableFilterBarEnabled = useIsTableFilterBarEnabled()
const [isTableEditorNewFilterBannerDismissed] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''),
@@ -29,7 +31,7 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
useEffect(() => {
if (!isPermissionsLoaded) return
if (canReadTables && !isTableEditorNewFilterBannerDismissed) {
if (canReadTables && !isTableEditorNewFilterBannerDismissed && !isTableFilterBarEnabled) {
addBanner({
id: TABLE_EDITOR_NEW_FILTER_BANNER_ID,
priority: 2,
@@ -49,6 +51,7 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
canReadTables,
isPermissionsLoaded,
isTableEditorNewFilterBannerDismissed,
isTableFilterBarEnabled,
])
if (isPermissionsLoaded && !canReadTables) {
@@ -1,6 +1,15 @@
import { Plus, Settings, X } from 'lucide-react'
import { Clipboard, Ellipsis, Plus, Settings, X } from 'lucide-react'
import { useState } from 'react'
import { AiIconAnimation, Button } from 'ui'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import {
AiIconAnimation,
Button,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { ButtonTooltip } from '../ButtonTooltip'
@@ -26,6 +35,7 @@ export const AIAssistantHeader = ({
isHipaaProjectDisallowed,
aiOptInLevel,
}: AIAssistantHeaderProps) => {
const snap = useAiAssistantStateSnapshot()
const [isOptInModalOpen, setIsOptInModalOpen] = useState(false)
return (
<div className="z-30 sticky top-0">
@@ -70,6 +80,26 @@ export const AIAssistantHeader = ({
content: { side: 'bottom', text: 'Permission settings' },
}}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonTooltip
type="text"
size="tiny"
icon={<Ellipsis strokeWidth={1.5} />}
className="h-7 w-7 p-0"
tooltip={{ content: { side: 'bottom', text: 'More options' } }}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="gap-x-2"
onClick={() => copyToClipboard(snap.activeChatId ?? '')}
>
<Clipboard size={14} strokeWidth={1.5} />
<span>Copy chat ID</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ButtonTooltip
type="text"
className="w-7 h-7"
@@ -1,8 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { handleError, patch } from 'data/fetchers'
import { toast } from 'sonner'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { authKeys } from './keys'
export type UserUpdateVariables = {
@@ -37,8 +37,11 @@ export const useUserUpdateMutation = ({
return useMutation<UserUpdateData, ResponseError, UserUpdateVariables>({
mutationFn: (vars) => updateUser(vars),
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) })
const { projectRef, userId } = variables
await Promise.all([
queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) }),
queryClient.invalidateQueries({ queryKey: authKeys.user(projectRef, userId) }),
])
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,21 +1,53 @@
import { IS_PLATFORM } from 'common'
import { ProjectSettingsVariables, useProjectSettingsV2Query } from './project-settings-v2-query'
import { useProjectSettingsV2Query } from './project-settings-v2-query'
import { useCustomDomainsQuery } from '@/data/custom-domains/custom-domains-query'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
export const useProjectEndpointQuery = ({ projectRef }: ProjectSettingsVariables) => {
return useProjectSettingsV2Query(
{ projectRef },
{
select: (data) => {
const protocol = data?.app_config?.protocol ?? 'https'
const endpoint = data?.app_config?.endpoint
const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}`
const storageEndpoint = data?.app_config?.storage_endpoint
? `${IS_PLATFORM ? 'https' : protocol}://${data?.app_config?.storage_endpoint}`
: undefined
export const useProjectApiUrl = (
{ projectRef }: { projectRef?: string },
{ enabled = true }: { enabled?: boolean } = {}
) => {
const { data } = useProjectAddonsQuery({ projectRef })
const hasCustomDomainsAddon = !!data?.selected_addons.find((x) => x.type === 'custom_domain')
return { endpoint: clientEndpoint, storageEndpoint }
},
}
)
const {
data: customDomainData,
error: customDomainsError,
isPending: isLoadingCustomDomains,
isSuccess: isSuccessCustomDomains,
isError: isErrorCustomDomains,
} = useCustomDomainsQuery({ projectRef }, { enabled })
const isCustomDomainsActive = customDomainData?.customDomain?.status === 'active'
const customEndpoint =
hasCustomDomainsAddon && isCustomDomainsActive
? `https://${customDomainData?.customDomain?.hostname}`
: undefined
const {
data: settings,
error: projectSettingsError,
isPending: isLoadingProjectSettings,
isSuccess: isSuccessProjectSettings,
isError: isErrorProjectSettings,
} = useProjectSettingsV2Query({ projectRef }, { enabled })
const protocol = settings?.app_config?.protocol ?? 'https'
const endpoint = settings?.app_config?.endpoint
const hostEndpoint = isSuccessProjectSettings ? `${protocol}://${endpoint}` : undefined
const resolvedEndpoint = isCustomDomainsActive ? customEndpoint : hostEndpoint
const storageEndpoint = settings?.app_config?.storage_endpoint
? `${IS_PLATFORM ? 'https' : protocol}://${settings?.app_config?.storage_endpoint}`
: undefined
return {
data: resolvedEndpoint,
customEndpoint,
hostEndpoint,
storageEndpoint,
error: projectSettingsError || (hasCustomDomainsAddon ? customDomainsError : undefined),
isPending: isLoadingProjectSettings || (hasCustomDomainsAddon && isLoadingCustomDomains),
isSuccess: isSuccessProjectSettings && (!hasCustomDomainsAddon || isSuccessCustomDomains),
isError: isErrorProjectSettings || (hasCustomDomainsAddon && isErrorCustomDomains),
}
}
@@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { IS_PLATFORM } from 'lib/constants'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { customDomainKeys } from './keys'
export type CustomDomainsVariables = {
@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query'
import { components } from 'api-types'
import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
import { handleError } from 'data/fetchers'
import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { useAuthConfigQuery } from '../auth/auth-config-query'
import { oauthServerAppKeys } from './keys'
@@ -46,9 +46,7 @@ export const useOAuthServerAppsQuery = <TData = OAuthServerAppsData>(
...options
}: UseCustomQueryOptions<OAuthServerAppsData, OAuthServerAppsError, TData> = {}
) => {
const { data: endpointData } = useProjectEndpointQuery({ projectRef })
const clientEndpoint = endpointData?.endpoint
const { hostEndpoint: clientEndpoint } = useProjectApiUrl({ projectRef })
const { data: authConfig, isSuccess: isSuccessConfig } = useAuthConfigQuery({ projectRef })
const isOAuthServerEnabled = !!authConfig?.OAUTH_SERVER_ENABLED
+23 -2
View File
@@ -3,15 +3,36 @@ import DefaultLayout from 'components/layouts/DefaultLayout'
import OrganizationLayout from 'components/layouts/OrganizationLayout'
import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout'
import type { NextPageWithLayout } from 'types'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
const OrgOAuthApps: NextPageWithLayout = () => {
return <OAuthApps />
return (
<>
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>OAuth Apps</PageHeaderTitle>
<PageHeaderDescription>
Published and authorized OAuth applications
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<OAuthApps />
</>
)
}
OrgOAuthApps.getLayout = (page) => (
<DefaultLayout>
<OrganizationLayout>
<OrganizationSettingsLayout>{page}</OrganizationSettingsLayout>
<OrganizationSettingsLayout pageTitle="OAuth Apps">{page}</OrganizationSettingsLayout>
</OrganizationLayout>
</DefaultLayout>
)
+18 -1
View File
@@ -5,6 +5,13 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import type { NextPageWithLayout } from 'types'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { LogoLoader } from 'ui'
const OrgAuditLogs: NextPageWithLayout = () => {
@@ -13,6 +20,16 @@ const OrgAuditLogs: NextPageWithLayout = () => {
return (
<>
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Audit Logs</PageHeaderTitle>
<PageHeaderDescription>
Organization-level activity history and security event records
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
{selectedOrganization === undefined && isLoadingPermissions ? <LogoLoader /> : <AuditLogs />}
</>
)
@@ -21,7 +38,7 @@ const OrgAuditLogs: NextPageWithLayout = () => {
OrgAuditLogs.getLayout = (page) => (
<DefaultLayout>
<OrganizationLayout>
<OrganizationSettingsLayout>{page}</OrganizationSettingsLayout>
<OrganizationSettingsLayout pageTitle="Audit Logs">{page}</OrganizationSettingsLayout>
</OrganizationLayout>
</DefaultLayout>
)
+20 -2
View File
@@ -3,6 +3,13 @@ import { Documents } from 'components/interfaces/Organization/Documents/Document
import DefaultLayout from 'components/layouts/DefaultLayout'
import OrganizationLayout from 'components/layouts/OrganizationLayout'
import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout'
import {
ScaffoldContainer,
ScaffoldDescription,
ScaffoldDivider,
ScaffoldHeader,
ScaffoldTitle,
} from 'components/layouts/Scaffold'
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import type { NextPageWithLayout } from 'types'
@@ -16,13 +23,24 @@ const OrgDocuments: NextPageWithLayout = () => {
return <UnknownInterface urlBack={`/org/${slug}`} />
}
return <Documents />
return (
<>
<ScaffoldContainer>
<ScaffoldHeader>
<ScaffoldTitle>Legal documents</ScaffoldTitle>
<ScaffoldDescription>Compliance documentation and legal agreements</ScaffoldDescription>
</ScaffoldHeader>
</ScaffoldContainer>
<ScaffoldDivider />
<Documents />
</>
)
}
OrgDocuments.getLayout = (page) => (
<DefaultLayout>
<OrganizationLayout>
<OrganizationSettingsLayout>{page}</OrganizationSettingsLayout>
<OrganizationSettingsLayout pageTitle="Legal Documents">{page}</OrganizationSettingsLayout>
</OrganizationLayout>
</DefaultLayout>
)
+26 -6
View File
@@ -5,6 +5,14 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz
import { usePermissionsQuery } from 'data/permissions/permissions-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import type { NextPageWithLayout } from 'types'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { LogoLoader } from 'ui'
const OrgGeneralSettings: NextPageWithLayout = () => {
@@ -13,11 +21,23 @@ const OrgGeneralSettings: NextPageWithLayout = () => {
return (
<>
{selectedOrganization === undefined && isLoadingPermissions ? (
<LogoLoader />
) : (
<GeneralSettings />
)}
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Organization Settings</PageHeaderTitle>
<PageHeaderDescription>
General configuration, privacy, and lifecycle controls
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
{selectedOrganization === undefined && isLoadingPermissions ? (
<LogoLoader />
) : (
<GeneralSettings />
)}
</PageContainer>
</>
)
}
@@ -25,7 +45,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => {
OrgGeneralSettings.getLayout = (page) => (
<DefaultLayout>
<OrganizationLayout>
<OrganizationSettingsLayout>{page}</OrganizationSettingsLayout>
<OrganizationSettingsLayout pageTitle="General">{page}</OrganizationSettingsLayout>
</OrganizationLayout>
</DefaultLayout>
)
+23 -2
View File
@@ -6,6 +6,13 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import type { NextPageWithLayout } from 'types'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
const OrgSecuritySettings: NextPageWithLayout = () => {
const { slug } = useParams()
@@ -15,13 +22,27 @@ const OrgSecuritySettings: NextPageWithLayout = () => {
return <UnknownInterface urlBack={`/org/${slug}`} />
}
return <SecuritySettings />
return (
<>
<PageHeader size="small">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Security</PageHeaderTitle>
<PageHeaderDescription>
Organization-wide security controls and MFA enforcement
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<SecuritySettings />
</>
)
}
OrgSecuritySettings.getLayout = (page) => (
<DefaultLayout>
<OrganizationLayout>
<OrganizationSettingsLayout>{page}</OrganizationSettingsLayout>
<OrganizationSettingsLayout pageTitle="Security">{page}</OrganizationSettingsLayout>
</OrganizationLayout>
</DefaultLayout>
)
+23 -2
View File
@@ -6,6 +6,13 @@ import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/Organiz
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import type { NextPageWithLayout } from 'types'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
const OrgSSO: NextPageWithLayout = () => {
const { slug } = useParams()
@@ -15,13 +22,27 @@ const OrgSSO: NextPageWithLayout = () => {
return <UnknownInterface urlBack={`/org/${slug}/general`} />
}
return <SSOConfig />
return (
<>
<PageHeader size="small">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Single Sign-On</PageHeaderTitle>
<PageHeaderDescription>
SAML SSO configuration and domain access controls
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<SSOConfig />
</>
)
}
OrgSSO.getLayout = (page) => (
<DefaultLayout>
<OrganizationLayout>
<OrganizationSettingsLayout>{page}</OrganizationSettingsLayout>
<OrganizationSettingsLayout pageTitle="SSO">{page}</OrganizationSettingsLayout>
</OrganizationLayout>
</DefaultLayout>
)
+9 -8
View File
@@ -29,7 +29,6 @@ import {
formatTime,
getFilesDataTransferItems,
getPathAlongFoldersToIndex,
getPathAlongOpenedFolders,
sanitizeNameForDuplicateInColumn,
validateFolderName,
} from '@/components/interfaces/Storage/StorageExplorer/StorageExplorer.utils'
@@ -37,7 +36,7 @@ import { convertFromBytes } from '@/components/interfaces/Storage/StorageSetting
import { InlineLink } from '@/components/ui/InlineLink'
import { getOrRefreshTemporaryApiKey } from '@/data/api-keys/temp-api-keys-utils'
import { configKeys } from '@/data/config/keys'
import { useProjectEndpointQuery } from '@/data/config/project-endpoint-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import type { ProjectStorageConfigResponse } from '@/data/config/project-storage-config-query'
import { getQueryClient } from '@/data/query-client'
import { deleteBucketObject } from '@/data/storage/bucket-object-delete-mutation'
@@ -1870,9 +1869,11 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG))
const stateRef = useLatest(state)
const { data: endpointData, isSuccess: isSuccessSettings } = useProjectEndpointQuery({
projectRef: project?.ref,
})
const {
storageEndpoint,
hostEndpoint,
isSuccess: isSuccessSettings,
} = useProjectApiUrl({ projectRef: project?.ref })
// [Joshen] JFYI opting with the useEffect here as the storage explorer state was being loaded
// before the project details were ready, hence the store kept returning project ref as undefined
@@ -1884,7 +1885,7 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
const storeAlreadyLoaded = state.projectRef === project?.ref
if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) {
const clientEndpoint = endpointData.storageEndpoint ?? endpointData.endpoint
const clientEndpoint = storageEndpoint ?? hostEndpoint ?? ''
const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable`
setState(
createStorageExplorerState({
@@ -1901,8 +1902,8 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
project?.connectionString,
stateRef,
isPaused,
endpointData?.endpoint,
endpointData?.storageEndpoint,
hostEndpoint,
storageEndpoint,
isSuccessSettings,
])
+2
View File
@@ -1,5 +1,6 @@
import type { GoPageInput } from 'marketing'
import byocEarlyAccess from './pre-release/byoc-early-access'
import exampleLeadGen from './lead-gen/example-lead-gen'
import exampleLegal from './legal/example-legal'
import exampleThankYou from './thank-you/example-thank-you'
@@ -7,6 +8,7 @@ import boltWebinar from './webinar/bolt-webinar'
import boltWebinarThankYou from './webinar/bolt-webinar-thank-you'
const pages: GoPageInput[] = [
byocEarlyAccess,
exampleLeadGen,
exampleThankYou,
exampleLegal,
@@ -0,0 +1,127 @@
import type { GoPageInput } from 'marketing'
const page: GoPageInput = {
template: 'lead-gen',
slug: 'byoc-early-access',
metadata: {
title: 'Bring Your Own Cloud (BYOC) for Supabase — Early Access',
description:
'Deploy Supabase in your own AWS account. Meet strict data residency and compliance requirements while Supabase handles operations, upgrades and monitoring.',
ogImage: '/images/landing-pages/byoc-early-access/og.png',
},
hero: {
title: 'Bring Your Own Cloud (BYOC) for Supabase',
subtitle: 'Early Access',
description:
'Deploy Supabase in your own AWS account. Meet strict data residency and compliance requirements while Supabase handles operations, upgrades and monitoring.',
ctas: [
{
label: 'Request Early Access',
href: '#form',
variant: 'primary',
},
],
},
sections: [
{
type: 'feature-grid',
title: 'Your cloud, operated by Supabase',
description: 'Get the full power of Supabase deployed inside your own infrastructure.',
items: [
{
title: 'Control where your data goes',
description:
'Meet data residency and compliance requirements. Your data stays in your infrastructure and in your region.',
},
{
title: 'Deploy the infrastructure you want',
description:
'Choose instance sizes and volumes for your use case. No project size constraints.',
},
{
title: 'Leverage your cloud costs',
description:
'Apply pre-negotiated discounts and cloud credits to your Supabase deployment.',
},
{
title: 'Let Supabase manage operations',
description:
'Supabase handles deployments, upgrades, monitoring and support. No Ops overhead.',
},
],
},
{
type: 'form',
id: 'form',
title: 'Early Access Request Form',
description:
"If you are interested in participating in BYOC early access when it becomes available later in 2026, please fill out the form below. A member of the Supabase team will reach out if you've been selected.",
fields: [
{
type: 'text',
name: 'first_name',
label: 'First Name',
placeholder: 'First Name',
required: true,
half: true,
},
{
type: 'text',
name: 'last_name',
label: 'Last Name',
placeholder: 'Last Name',
required: true,
half: true,
},
{
type: 'email',
name: 'email',
label: 'Email Address',
placeholder: 'Work email',
required: true,
},
{
type: 'text',
name: 'company_name',
label: 'Company Name',
placeholder: 'Company name',
required: true,
},
{
type: 'text',
name: 'supabase_org_name',
label: 'Supabase Organization Name',
placeholder: 'Organization name (if applicable)',
required: false,
},
],
submitLabel: 'Request Early Access',
disclaimer:
'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy).',
crm: {
hubspot: {
formGuid: 'c09ead14-e1b7-4031-9f77-ba6b2ad96364',
fieldMap: {
first_name: 'firstname',
last_name: 'lastname',
email: 'email',
company_name: 'company',
supabase_org_name: 'what_is_your_supabase_org_slug',
},
consent:
'By submitting this form, I confirm that I have read and understood the Privacy Policy.',
},
customerio: {
event: 'early_access_requested',
profileMap: {
email: 'email',
first_name: 'first_name',
last_name: 'last_name',
},
},
},
},
],
}
export default page
@@ -16,9 +16,9 @@ type LinkProps = {
export const SolutionsDropdown = () => (
<div className="flex flex-col xl:flex-row">
<div className="w-[550px] xl:w-[470px] py-8 px-8 bg-background grid gap-3 grid-cols-2">
{/* two first columns */}
{DevelopersData['navigation'].slice(0, 2).map((column) => (
<div className="w-[550px] xl:w-[600px] py-8 px-8 bg-background grid gap-3 grid-cols-2 xl:grid-cols-3">
{/* Skill Level, Who it's for, App Type */}
{DevelopersData['navigation'].slice(0, 3).map((column) => (
<LinksGroup key={column.label} links={column.links} label={column.label} />
))}
</div>
@@ -26,11 +26,11 @@ export const SolutionsDropdown = () => (
<div className="bg-surface-75 flex flex-col w-[550px] xl:w-[480px] border-t xl:border-t-0 xl:border-l">
<div className="flex flex-col gap-6 py-8 px-10">
<label className="text-foreground-lighter text-xs uppercase tracking-widest font-mono">
{DevelopersData['navigation'][2].label}
{DevelopersData['navigation'][3].label}
</label>
<div className="flex flex-col gap-4">
{DevelopersData['navigation'][2].links.map((link) => (
{DevelopersData['navigation'][3].links.map((link) => (
<MigrationLinkCard key={link.text} link={link} />
))}
</div>
@@ -0,0 +1,72 @@
import Link from 'next/link'
import { ArrowRight, Check } from 'lucide-react'
import SectionContainer from 'components/Layouts/SectionContainer'
export interface CustomerEvidenceCustomer {
name: string
logo?: string
highlights: string[]
cta?: { label: string; href: string }
}
export interface CustomerEvidenceSectionProps {
id: string
heading: React.ReactNode
customers: CustomerEvidenceCustomer[]
className?: string
}
const CustomerEvidenceSection = ({
id,
heading,
customers,
className = '',
}: CustomerEvidenceSectionProps) => (
<SectionContainer id={id} className={`py-16 md:py-24 ${className}`}>
<div className="text-center max-w-3xl mx-auto mb-12">
<h2 className="text-2xl sm:text-3xl lg:text-4xl text-foreground-lighter">{heading}</h2>
</div>
<div className="grid md:grid-cols-2 gap-0 max-w-5xl mx-auto rounded-lg border overflow-hidden divide-x">
{customers.map((customer) => (
<div key={customer.name} className="p-6 md:p-8 flex flex-col">
{customer.logo && (
<div
className="h-8 w-28 mb-6"
style={{
maskImage: `url(${customer.logo})`,
WebkitMaskImage: `url(${customer.logo})`,
maskSize: 'contain',
WebkitMaskSize: 'contain',
maskRepeat: 'no-repeat',
WebkitMaskRepeat: 'no-repeat',
maskPosition: 'left center',
WebkitMaskPosition: 'left center',
backgroundColor: 'currentColor',
}}
/>
)}
<h3 className="text-lg text-foreground font-medium mb-4">{customer.name}</h3>
<ul className="space-y-3 mb-6 flex-grow">
{customer.highlights.map((highlight, i) => (
<li key={i} className="flex items-start gap-3 text-foreground-lighter text-sm">
<Check className="w-4 h-4 text-brand shrink-0 mt-0.5" />
<span>{highlight}</span>
</li>
))}
</ul>
{customer.cta && (
<Link
href={customer.cta.href}
className="inline-flex items-center gap-2 text-sm text-brand hover:text-brand-600 transition-colors"
>
{customer.cta.label}
<ArrowRight className="w-4 h-4" />
</Link>
)}
</div>
))}
</div>
</SectionContainer>
)
export default CustomerEvidenceSection
@@ -0,0 +1,41 @@
import Link from 'next/link'
import { ArrowUpRight } from 'lucide-react'
import { Button } from 'ui'
import SectionContainer from 'components/Layouts/SectionContainer'
export interface WhatItTakesItem {
id: string
description: React.ReactNode
url: string
linkLabel?: string
}
export interface WhatItTakesSectionProps {
id: string
heading: React.ReactNode
items: WhatItTakesItem[]
className?: string
}
const WhatItTakesSection = ({ id, heading, items, className = '' }: WhatItTakesSectionProps) => (
<SectionContainer id={id} className={`py-16 md:py-24 ${className}`}>
<div className="flex flex-col gap-8">
<h2 className="h2 text-foreground-lighter">{heading}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
{items.map((item) => (
<div
key={item.id}
className="flex flex-col gap-3 p-6 rounded-lg border border-default bg-surface-75"
>
<p className="text-foreground-light">{item.description}</p>
<Button asChild type="default" size="tiny" icon={<ArrowUpRight className="w-3 h-3" />}>
<Link href={item.url}>{item.linkLabel ?? 'Read more'}</Link>
</Button>
</div>
))}
</div>
</div>
</SectionContainer>
)
export default WhatItTakesSection
+6 -3
View File
@@ -14,15 +14,16 @@ import {
import SectionContainer from 'components/Layouts/SectionContainer'
import {
SolutionTypes,
appTypeSolutions,
migrationSolutions,
skillBasedSolutions,
useCaseSolutions,
migrationSolutions,
} from 'data/Solutions'
interface Props {
activeItem: SolutionTypes
className?: string
type?: 'skill-based' | 'use-case' | 'migration'
type?: 'skill-based' | 'use-case' | 'migration' | 'app-type'
}
function SolutionsStickyNav({ type, activeItem, className }: Props) {
@@ -32,7 +33,9 @@ function SolutionsStickyNav({ type, activeItem, className }: Props) {
? skillBasedSolutions.solutions
: type === 'use-case'
? useCaseSolutions.solutions
: migrationSolutions.solutions
: type === 'app-type'
? appTypeSolutions.solutions
: migrationSolutions.solutions
const items = solutions.map((solution: any) => ({
id: solution.id,
name: solution.text,
+50
View File
@@ -3,9 +3,11 @@ import {
Building2Icon,
Code2Icon,
HammerIcon,
Heart,
LightbulbIcon,
PointerIcon,
PuzzleIcon,
ShieldCheck,
TrendingUpIcon,
UsersIcon,
ZapIcon,
@@ -27,6 +29,10 @@ export enum Solutions {
hackathon = 'hackathon',
innovationTeams = 'innovation-teams',
vibeCoders = 'vibe-coders',
b2bSaaS = 'b2b-saas',
finserv = 'finserv',
healthcare = 'healthcare',
agents = 'agents',
}
export const skillBasedSolutions = {
@@ -145,6 +151,40 @@ export const useCaseSolutions = {
],
}
export const appTypeSolutions = {
label: 'Solutions',
solutions: [
{
id: Solutions.b2bSaaS,
text: 'B2B SaaS',
description: '',
url: '/solutions/b2b-saas',
icon: Building2Icon,
},
{
id: Solutions.finserv,
text: 'FinServ',
description: '',
url: '/solutions/finserv',
icon: ShieldCheck,
},
{
id: Solutions.healthcare,
text: 'Healthcare',
description: '',
url: '/solutions/healthcare',
icon: Heart,
},
{
id: Solutions.agents,
text: 'Agents',
description: '',
url: '/solutions/agents',
icon: BotIcon,
},
],
}
export const migrationSolutions = {
label: 'Solutions',
solutions: [
@@ -241,6 +281,16 @@ export const navData = {
})),
],
},
{
label: 'App Type',
links: [
...appTypeSolutions.solutions.map((solution) => ({
text: solution.text,
url: solution.url,
icon: solution.icon,
})),
],
},
{
label: 'Migration',
links: [
+3 -2
View File
@@ -545,8 +545,9 @@ const data: {
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M21.7152 12.8129C21.7152 11.4644 21.4115 10.1867 20.8688 9.0447M12.925 21.6032C14.2829 21.6032 15.5689 21.2952 16.717 20.7454M16.717 20.7454C17.2587 21.5257 18.1612 22.0366 19.1831 22.0366C20.84 22.0366 22.1831 20.6935 22.1831 19.0366C22.1831 17.3798 20.84 16.0366 19.1831 16.0366C17.5263 16.0366 16.1831 17.3798 16.1831 19.0366C16.1831 19.6716 16.3804 20.2605 16.717 20.7454ZM4.96506 16.5471C4.16552 17.086 3.63965 17.9999 3.63965 19.0366C3.63965 20.6935 4.98279 22.0366 6.63965 22.0366C8.2965 22.0366 9.63965 20.6935 9.63965 19.0366C9.63965 17.3798 8.2965 16.0366 6.63965 16.0366C6.01951 16.0366 5.44333 16.2248 4.96506 16.5471ZM9.12614 4.88371C8.58687 4.08666 7.67444 3.56274 6.63965 3.56274C4.98279 3.56274 3.63965 4.90589 3.63965 6.56274C3.63965 8.2196 4.98279 9.56274 6.63965 9.56274C8.2965 9.56274 9.63965 8.2196 9.63965 6.56274C9.63965 5.94069 9.45032 5.36285 9.12614 4.88371ZM20.8688 9.0447C21.6621 8.50486 22.1831 7.59464 22.1831 6.56274C22.1831 4.90589 20.84 3.56274 19.1831 3.56274C17.5263 3.56274 16.1831 4.90589 16.1831 6.56274C16.1831 8.2196 17.5263 9.56274 19.1831 9.56274C19.8081 9.56274 20.3884 9.37165 20.8688 9.0447Z',
subheading: (
<>
Connect Supabase to <span className="text-foreground">Redshift, BigQuery, MySQL</span>{' '}
and external APIs for seamless integrations with your clients’ existing systems.
Connect Supabase to{' '}
<span className="text-foreground">BigQuery, Snowflake, ClickHouse</span> and external
APIs for seamless integrations with your clients’ existing systems.
</>
),
image: (
+349
View File
@@ -0,0 +1,349 @@
import {
Check,
ClipboardCheck,
FolderLock,
HeartPulse,
Lightbulb,
List,
Lock,
ShieldAlert,
ShieldCheck,
Users,
UserX,
} from 'lucide-react'
import { CubeIcon } from '@heroicons/react/outline'
import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection'
import { frameworks } from 'components/Hero/HeroFrameworks'
import type { FeatureGridProps } from 'components/Solutions/FeatureGrid'
import type { PlatformSectionProps } from 'components/Solutions/PlatformSection'
import type { ResultsSectionProps } from 'components/Solutions/ResultsSection'
import type { SecuritySectionProps } from 'components/Enterprise/Security'
import {
FrameworkLink,
type FeaturesSection,
type HeroSection,
type Metadata,
} from './solutions.utils'
import { getSharedSections } from './shared-sections'
import { Image } from 'ui'
import { useSendTelemetryEvent } from 'lib/telemetry'
const data: () => {
metadata: Metadata
heroSection: HeroSection
singleQuote: {
id: string
quote: {
text: string
author: string
role: string
logo?: React.ReactElement
link?: string
}
}
why: FeaturesSection
platform: PlatformSectionProps
developerExperience: ReturnType<typeof getSharedSections>['developerExperience']
resultsSection: ResultsSectionProps
featureGrid: FeatureGridProps
securitySection: SecuritySectionProps
ecosystemSection: FeaturesSection
platformStarterSection: TwoColumnsSectionProps
customerEvidence: {
id: string
heading: React.ReactNode
customers: Array<{
name: string
logo?: string
highlights: string[]
cta?: { label: string; href: string }
}>
}
} = () => {
const shared = getSharedSections()
const sendTelemetryEvent = useSendTelemetryEvent()
return {
metadata: {
metaTitle: 'Supabase for Agents',
metaDescription:
'One platform for your agents. Memory, tools, and data in one place. Stop stitching together separate services.',
},
heroSection: {
id: 'hero',
title: 'Supabase for Agents',
h1: (
<>
One platform for your agents.
<br />
<span className="text-foreground block">Memory, tools, and data in one place.</span>
</>
),
subheader: [
<>
Stop stitching together separate services for memory, vectors, auth, file storage, and
APIs. Supabase gives your agents a complete Postgres backend with everything they need,
from one dashboard, one connection string, one bill.
</>,
],
image: undefined,
ctas: [
{
label: 'Start your project',
href: 'https://supabase.com/dashboard',
type: 'primary' as any,
onClick: () =>
sendTelemetryEvent({
action: 'start_project_button_clicked',
properties: { buttonLocation: 'Solutions: Agents page hero' },
}),
},
{
label: 'Request a demo',
href: 'https://supabase.com/contact/sales',
type: 'default' as any,
onClick: () =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'Solutions: Agents page hero' },
}),
},
],
},
singleQuote: {
id: 'social-proof',
quote: {
text: '"Supabase is great because it has everything. I don\'t need a different solution for authentication, a different solution for database, or a different solution for storage."',
author: 'Yasser Elsaid',
role: 'Founder, Chatbase',
link: '/customers/chatbase',
logo: (
<>
<Image
src="/images/customers/logos/chatbase.png"
alt="Chatbase"
width={128}
height={48}
className="object-contain w-24 md:w-32 dark:hidden"
/>
<Image
src="/images/customers/logos/light/chatbase.png"
alt="Chatbase"
width={128}
height={48}
className="object-contain w-24 md:w-32 hidden dark:block"
/>
</>
),
},
},
why: {
id: 'why-supabase',
label: '',
heading: (
<>
Why <span className="text-foreground">agent builders</span> choose Supabase
</>
),
subheading:
'You need infrastructure that keeps up with agents that plan, act, and learn autonomously. Supabase is the complete Postgres developer platform built for agentic workloads.',
features: [
{
id: 'one-platform',
icon: CubeIcon,
heading: 'One platform, not five.',
subheading:
'Most agent stacks require a vector database, an auth provider, a file store, an API layer, and a separate Postgres instance. Supabase replaces all of them. One connection string, one dashboard, one bill.',
},
{
id: 'security-at-agent-speed',
icon: Lock,
heading: 'Security at agent speed.',
subheading:
'Agents make autonomous decisions with no human in the loop. Row Level Security ensures every database call respects tenant boundaries. Combined with SOC 2 Type II certification and database audit logs, your agent workloads stay secure at production scale.',
},
{
id: 'native-mcp',
icon: (props: any) => (
<svg
width="23"
height="23"
viewBox="0 0 25 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M19 5L22 2M2 22L5 19M7.5 13.5L10 11M10.5 16.5L13 14M6.3 20.3C6.52297 20.5237 6.78791 20.7013 7.07963 20.8224C7.37136 20.9435 7.68413 21.0059 8 21.0059C8.31587 21.0059 8.62864 20.9435 8.92036 20.8224C9.21209 20.7013 9.47703 20.5237 9.7 20.3L12 18L6 12L3.7 14.3C3.47626 14.523 3.29873 14.7879 3.17759 15.0796C3.05646 15.3714 2.99411 15.6841 2.99411 16C2.99411 16.3159 3.05646 16.6286 3.17759 16.9204C3.29873 17.2121 3.47626 17.477 3.7 17.7L6.3 20.3ZM12 6L18 12L20.3 9.7C20.5237 9.47703 20.7013 9.21209 20.8224 8.92036C20.9435 8.62864 21.0059 8.31587 21.0059 8C21.0059 7.68413 20.9435 7.37136 20.8224 7.07963C20.7013 6.78791 20.5237 6.52297 20.3 6.3L17.7 3.7C17.477 3.47626 17.2121 3.29873 16.9204 3.17759C16.6286 3.05646 16.3159 2.99411 16 2.99411C15.6841 2.99411 15.3714 3.05646 15.0796 3.17759C14.7879 3.29873 14.523 3.47626 14.3 3.7L12 6Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
heading: 'Native MCP from day one.',
subheading:
'Supabase includes an MCP server that supports both read and write operations. Your agents can query schemas, insert records, and manage data through a standardized protocol.',
},
],
},
platform: {
...shared.platform,
title: (
<>
Supabase is the Postgres platform{' '}
<span className="text-foreground">your agents control</span>
</>
),
subheading: 'Everything your agents need to connect, query, and act on your data.',
},
developerExperience: shared.developerExperience,
resultsSection: shared.resultsSection,
featureGrid: shared.featureGrid,
securitySection: {
id: 'security',
label: 'Security',
heading: (
<>
Trusted for <span className="text-foreground">agent workloads in production</span>
</>
),
subheading:
'Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.',
features: [
{ icon: ShieldCheck, heading: 'SOC 2 Type II certified' },
{ icon: HeartPulse, heading: 'HIPAA compliant' },
{ icon: ShieldAlert, heading: 'DDoS Protection' },
{ icon: Lock, heading: 'Multi-factor Authentication' },
{ icon: ClipboardCheck, heading: 'Vulnerability Management' },
{ icon: Users, heading: 'Role-based access control' },
{ icon: List, heading: 'Database Audit Logs' },
{ icon: Lightbulb, heading: 'Security Advisors' },
{ icon: FolderLock, heading: 'Encrypted Storage' },
{ icon: UserX, heading: 'Network restrictions' },
],
cta: {
label: 'Learn about security',
url: '/security',
},
},
ecosystemSection: {
id: 'ecosystem',
label: '',
heading: (
<>
Works with every <span className="text-foreground">agent framework</span>
</>
),
subheading: 'Supabase integrates with the tools and frameworks agent builders already use.',
features: [
{
id: 'agent-frameworks',
icon: CubeIcon,
heading: 'Agent frameworks.',
subheading:
'LangChain and any framework that speaks SQL, REST, or MCP—including CrewAI, AutoGen, and others.',
},
{
id: 'ai-providers',
icon: Check,
heading: 'AI providers.',
subheading:
'OpenAI, Anthropic, Google, Mistral, and any model provider. Supabase is model-agnostic.',
},
{
id: 'development-tools',
icon: Check,
heading: 'Development tools.',
subheading:
'Cursor, Windsurf, VS Code Copilot, Claude Code, and every editor that supports MCP.',
},
],
},
platformStarterSection: {
id: 'quickstarts',
heading: (
<>
<span className="text-foreground">Get started</span>
</>
),
subheading:
'Copy these prompts into your AI editor to scaffold agent infrastructure on Supabase:',
docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts',
leftFooter: (
<div className="grid grid-cols-5 divide-x divide-y rounded-lg overflow-hidden border">
{frameworks.map((framework) => (
<FrameworkLink key={framework.name} framework={framework} />
))}
</div>
),
aiPrompts: [
{
id: 'agent-memory',
title: 'Build an agent memory store',
code: 'Create a Supabase schema for persistent agent memory with session tracking, JSONB state storage, and vector embeddings for semantic recall.',
language: 'markdown',
copyable: true,
},
{
id: 'rag-pipeline',
title: 'Set up a RAG pipeline',
code: 'Build a RAG pipeline on Supabase with document upload to Storage, text extraction via Edge Functions, and pgvector embeddings with hybrid search.',
language: 'markdown',
copyable: true,
},
{
id: 'multi-tenant',
title: 'Configure multi-tenant agent access',
code: "Design a multi-tenant Supabase schema with Row Level Security policies that restrict each agent to its own tenant's data.",
language: 'markdown',
copyable: true,
},
{
id: 'tool-endpoint',
title: 'Deploy a tool-calling endpoint',
code: 'Create a Supabase Edge Function that serves as a tool endpoint for an AI agent, accepting structured input, querying the database, and returning formatted results.',
language: 'markdown',
copyable: true,
},
],
},
customerEvidence: {
id: 'case-studies',
heading: (
<>
Customer <span className="text-foreground">stories</span>
</>
),
customers: [
{
name: 'Humata',
logo: '/images/customers/logos/humata.png',
highlights: ['AI-powered document analysis at scale'],
cta: { label: 'Read the case study', href: '/customers/humata' },
},
{
name: 'Chatbase',
logo: '/images/customers/logos/chatbase.png',
highlights: [
'One of the most successful single-founder AI products',
'Built entirely on Supabase',
],
cta: { label: 'Read the case study', href: '/customers/chatbase' },
},
{
name: 'Markprompt',
logo: '/images/customers/logos/markprompt.png',
highlights: ['GDPR-compliant AI chatbots on Supabase'],
cta: { label: 'Read the case study', href: '/customers/markprompt' },
},
],
},
}
}
export default data
+306
View File
@@ -0,0 +1,306 @@
import dynamic from 'next/dynamic'
import {
Check,
ClipboardCheck,
FolderLock,
HeartPulse,
Lightbulb,
List,
Lock,
ShieldAlert,
ShieldCheck,
Users,
UserX,
} from 'lucide-react'
import { CubeIcon } from '@heroicons/react/outline'
import { Image } from 'ui'
import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection'
import { frameworks } from 'components/Hero/HeroFrameworks'
import type { FeatureGridProps } from 'components/Solutions/FeatureGrid'
import type { PlatformSectionProps } from 'components/Solutions/PlatformSection'
import type { ResultsSectionProps } from 'components/Solutions/ResultsSection'
import type { SecuritySectionProps } from 'components/Enterprise/Security'
import {
FrameworkLink,
type FeaturesSection,
type HeroSection,
type Metadata,
} from './solutions.utils'
import { getSharedSections } from './shared-sections'
import { useBreakpoint } from 'common'
import { useSendTelemetryEvent } from 'lib/telemetry'
const data: () => {
metadata: Metadata
heroSection: HeroSection
singleQuote: {
id: string
quote: {
text: string
author: string
role: string
link?: string
logo?: React.ReactElement
}
}
why: FeaturesSection
platform: PlatformSectionProps
developerExperience: ReturnType<typeof getSharedSections>['developerExperience']
resultsSection: ResultsSectionProps
featureGrid: FeatureGridProps
securitySection: SecuritySectionProps
platformStarterSection: TwoColumnsSectionProps
customerEvidence: {
id: string
heading: React.ReactNode
customers: Array<{
name: string
logo?: string
highlights: string[]
cta?: { label: string; href: string }
}>
}
} = () => {
const isXs = useBreakpoint(640)
const shared = getSharedSections()
const sendTelemetryEvent = useSendTelemetryEvent()
return {
metadata: {
metaTitle: 'Supabase for B2B SaaS',
metaDescription:
'Ship faster. Scale smarter. Own your backend. Supabase gives B2B SaaS teams the tools to build, launch, and scale modern applications.',
},
heroSection: {
id: 'hero',
title: 'Supabase for B2B SaaS',
h1: (
<>
<span className="block text-foreground">Ship faster. Scale smarter.</span>
<span className="block md:ml-0">Own your backend.</span>
</>
),
subheader: [
<>
Supabase gives B2B SaaS teams the tools to build, launch, and scale modern applications
without backend complexity. Focus on product velocity, multi-tenant architecture, and zero
backend boilerplate.
</>,
],
image: undefined,
ctas: [
{
label: 'Start your project',
href: 'https://supabase.com/dashboard',
type: 'primary' as any,
onClick: () =>
sendTelemetryEvent({
action: 'start_project_button_clicked',
properties: { buttonLocation: 'Solutions: B2B SaaS page hero' },
}),
},
{
label: 'Request a demo',
href: 'https://supabase.com/contact/sales',
type: 'default' as any,
onClick: () =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'Solutions: B2B SaaS page hero' },
}),
},
],
},
singleQuote: {
id: 'social-proof',
quote: {
text: '"Supabase enabled us to focus on building the best email infrastructure for developers, without worrying about backend complexity."',
author: 'Zeno Rocha',
role: 'CEO, Resend',
link: '/customers/resend',
logo: (
<>
<Image
src="/images/customers/logos/resend.png"
alt="Resend"
width={128}
height={48}
className="object-contain w-24 md:w-32 dark:hidden"
/>
<Image
src="/images/customers/logos/light/resend.png"
alt="Resend"
width={128}
height={48}
className="object-contain w-24 md:w-32 hidden dark:block"
/>
</>
),
},
},
why: {
id: 'why-supabase',
label: '',
heading: (
<>
Why <span className="text-foreground">B2B SaaS companies</span> choose Supabase
</>
),
subheading:
'Build high-velocity SaaS products on a modern backend platform that balances developer speed with architectural integrity.',
features: [
{
id: 'multi-tenant',
icon: CubeIcon,
heading: 'Ship multi-tenant apps without the plumbing.',
subheading:
'RLS enforces tenant isolation at the database layer. RBAC controls what each user role can access. No custom middleware, no tenant-routing code, no data leakage risk.',
},
{
id: 'own-your-data',
icon: Lock,
heading: 'Own your data layer, avoid lock-in.',
subheading:
'Supabase is just Postgres. No proprietary query language, no vendor-specific APIs. Export your data, self-host if you need to, and keep full control of your architecture.',
},
{
id: 'scale',
icon: Check,
heading: 'Scale from first customer to enterprise tier.',
subheading:
'Start free, scale to millions of rows with read replicas, connection pooling, and HA failover. Same platform from prototype through SOC 2 audit.',
},
],
},
platform: {
...shared.platform,
title: (
<>
Supabase is the <span className="text-foreground">Postgres platform</span> you control
</>
),
subheading:
'Supabase includes everything you need to deliver robust, scalable, and dependable software as a service.',
},
developerExperience: shared.developerExperience,
resultsSection: shared.resultsSection,
featureGrid: shared.featureGrid,
securitySection: {
id: 'security',
label: 'Security',
heading: (
<>
Trusted for <span className="text-foreground">B2B SaaS solutions at every stage</span>
</>
),
subheading:
"Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers' data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.",
features: [
{ icon: ShieldCheck, heading: 'SOC 2 Type II certified' },
{ icon: HeartPulse, heading: 'HIPAA compliant' },
{ icon: ShieldAlert, heading: 'DDoS Protection' },
{ icon: Lock, heading: 'Multi-factor Authentication' },
{ icon: ClipboardCheck, heading: 'Vulnerability Management' },
{ icon: Users, heading: 'Role-based access control' },
{ icon: List, heading: 'Database Audit Logs' },
{ icon: Lightbulb, heading: 'Security Advisors' },
{ icon: FolderLock, heading: 'Encrypted Storage' },
{ icon: UserX, heading: 'Network restrictions' },
],
cta: {
label: 'Learn about security',
url: '/security',
},
},
platformStarterSection: {
id: 'quickstarts',
heading: (
<>
<span className="text-foreground">Choose your platform</span> to start building in seconds
</>
),
headingRight: (
<>
Or, start with <span className="text-foreground">Supabase AI Prompts</span>
</>
),
docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts',
leftFooter: (
<div className="grid grid-cols-5 divide-x divide-y rounded-lg overflow-hidden border">
{frameworks.map((framework) => (
<FrameworkLink key={framework.name} framework={framework} />
))}
</div>
),
aiPrompts: [
{
id: 'auth-setup',
title: 'Bootstrap Next.js app with Supabase Auth',
code: 'Set up Supabase Auth with Next.js: install @supabase/supabase-js and @supabase/ssr, configure environment variables, create browser and server clients, and add middleware for token refresh.',
language: 'markdown',
docsUrl:
'https://supabase.com/docs/guides/getting-started/ai-prompts/nextjs-supabase-auth',
},
{
id: 'rls-policies',
title: 'Create RLS policies',
code: "Generate Row Level Security policies for tenant isolation. Retrieve your schema, then write policies that restrict users to their tenant's data using the auth.uid() or custom claims.",
language: 'markdown',
docsUrl:
'https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies',
},
],
},
customerEvidence: {
id: 'case-studies',
heading: (
<>
Customer <span className="text-foreground">stories</span>
</>
),
customers: [
{
name: 'Resend',
logo: '/images/customers/logos/resend.png',
highlights: [
'Focus on building the best email infrastructure for developers',
'Backend complexity handled by Supabase',
],
cta: { label: 'Read the case study', href: '/customers/resend' },
},
{
name: 'Mobbin',
logo: '/images/customers/logos/mobbin.png',
highlights: [
'Migrated 200,000 users from Firebase',
'Better authentication experience at scale',
],
cta: { label: 'Read the case study', href: '/customers/mobbin' },
},
{
name: 'Shotgun',
logo: '/images/customers/logos/shotgun.png',
highlights: [
'83% reduction in data infrastructure costs',
'Remarkable database efficiency through migration',
],
cta: { label: 'Read the case study', href: '/customers/shotgun' },
},
{
name: 'Quilia',
logo: '/images/customers/logos/quilia.png',
highlights: [
'75% reduction in development time',
'50% lower costs with enhanced security for sensitive client data',
],
cta: { label: 'Read the case study', href: '/customers/quilia' },
},
],
},
}
}
export default data
+7 -6
View File
@@ -497,8 +497,9 @@ const data: () => {
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z',
subheading: (
<>
Connect Supabase to <span className="text-foreground">Redshift, BigQuery, MySQL</span>
, and external APIs for seamless integrations.
Connect Supabase to{' '}
<span className="text-foreground">BigQuery, Snowflake, ClickHouse</span>, and external
APIs for seamless integrations.
</>
),
image: (
@@ -633,10 +634,10 @@ const data: () => {
},
{
id: 'multi-region',
title: 'Multi-region Deployments',
title: 'Multi-region Options',
description: (
<>
Deploy databases across multiple regions for{' '}
Deploy in your chosen region with optional read replicas in other regions for{' '}
<span className="text-foreground">global availability</span>.
</>
),
@@ -649,8 +650,8 @@ const data: () => {
description: (
<>
Enterprise plans offer{' '}
<span className="text-foreground">automatic failover and redundancy</span> for
mission-critical applications.
<span className="text-foreground">failover and redundancy</span> for mission-critical
applications.
</>
),
icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557',
+311
View File
@@ -0,0 +1,311 @@
import {
Check,
ClipboardCheck,
FolderLock,
HeartPulse,
Lightbulb,
List,
Lock,
ShieldAlert,
ShieldCheck,
Users,
UserX,
} from 'lucide-react'
import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection'
import { frameworks } from 'components/Hero/HeroFrameworks'
import type { FeatureGridProps } from 'components/Solutions/FeatureGrid'
import type { PlatformSectionProps } from 'components/Solutions/PlatformSection'
import type { ResultsSectionProps } from 'components/Solutions/ResultsSection'
import type { SecuritySectionProps } from 'components/Enterprise/Security'
import {
FrameworkLink,
type FeaturesSection,
type HeroSection,
type Metadata,
} from './solutions.utils'
import { getSharedSections } from './shared-sections'
import MainProducts from '../MainProducts'
import { Image } from 'ui'
import { PRODUCT_SHORTNAMES } from 'shared-data/products'
import { useSendTelemetryEvent } from 'lib/telemetry'
const data: () => {
metadata: Metadata
heroSection: HeroSection
singleQuote: {
id: string
quote: {
text: string
author: string
role: string
link?: string
logo?: React.ReactElement
}
}
why: FeaturesSection
platform: PlatformSectionProps
developerExperience: ReturnType<typeof getSharedSections>['developerExperience']
resultsSection: ResultsSectionProps
featureGrid: FeatureGridProps
securitySection: SecuritySectionProps
platformStarterSection: TwoColumnsSectionProps
customerEvidence: {
id: string
heading: React.ReactNode
customers: Array<{
name: string
logo?: string
highlights: string[]
cta?: { label: string; href: string }
}>
}
} = () => {
const shared = getSharedSections()
const sendTelemetryEvent = useSendTelemetryEvent()
return {
metadata: {
metaTitle: 'Supabase for Financial Services',
metaDescription:
'Secure, compliant financial applications without the complexity. SOC 2, ACID transactions, real-time data, and audit trails built in.',
},
heroSection: {
id: 'hero',
title: 'Supabase for Financial Services',
h1: (
<>
<span className="block text-foreground">Secure, compliant financial applications</span>
<span className="block md:ml-0">without the complexity.</span>
</>
),
subheader: [
<>
Supabase is a Postgres development platform with SOC 2 certification, ACID transactions,
real-time data, and audit trails built in. Build for trading, payments, lending, and
embedded finance from one platform.
</>,
],
image: undefined,
ctas: [
{
label: 'Start your project',
href: 'https://supabase.com/dashboard',
type: 'primary' as any,
onClick: () =>
sendTelemetryEvent({
action: 'start_project_button_clicked',
properties: { buttonLocation: 'Solutions: FinServ page hero' },
}),
},
{
label: 'Request a demo',
href: 'https://supabase.com/contact/sales',
type: 'default' as any,
onClick: () =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'Solutions: FinServ page hero' },
}),
},
],
},
singleQuote: {
id: 'social-proof',
quote: {
text: '"We wanted a backend that could accelerate our development while maintaining security and scalability. Supabase stood out due to its automation, integrations, and ecosystem."',
author: 'Raunak Kathuria',
role: 'VP of Engineering, Deriv',
link: '/customers/deriv',
logo: (
<>
<Image
src="/images/customers/logos/deriv.png"
alt="Deriv"
width={128}
height={48}
className="object-contain w-24 md:w-32 dark:hidden"
/>
<Image
src="/images/customers/logos/light/deriv.png"
alt="Deriv"
width={128}
height={48}
className="object-contain w-24 md:w-32 hidden dark:block"
/>
</>
),
},
},
why: {
id: 'why-supabase',
label: '',
heading: (
<>
Why <span className="text-foreground">financial services companies</span> choose Supabase
</>
),
subheading: 'Build secure, scalable financial applications using a trusted data platform.',
features: [
{
id: 'compliance',
icon: ShieldCheck,
heading: 'Compliance you can prove, not just claim.',
subheading:
'SOC 2 Type II certified with comprehensive audit logs, encryption at rest and in transit, and database-level access controls. Meet regulatory requirements with infrastructure that documents every action automatically.',
},
{
id: 'transactional-integrity',
icon: Check,
heading: 'Transactional integrity at every layer.',
subheading:
'Postgres provides full ACID compliance for settlements, reversals, and multi-step financial operations. No partial writes, no eventual consistency surprises. Your ledger stays correct.',
},
{
id: 'realtime-data',
icon: MainProducts[PRODUCT_SHORTNAMES.REALTIME].icon,
heading: 'Real-time data for real-time decisions.',
subheading:
'Process transactions, detect fraud signals, and push updates to dashboards as they happen. Realtime subscriptions and Edge Functions give your applications the speed financial workloads demand.',
},
],
},
platform: {
...shared.platform,
title: (
<>
Supabase is the <span className="text-foreground">SOC 2-compliant Postgres platform</span>{' '}
you control
</>
),
subheading:
'Supabase includes everything you need to build secure, compliant financial applications.',
},
developerExperience: shared.developerExperience,
resultsSection: shared.resultsSection,
featureGrid: shared.featureGrid,
securitySection: {
id: 'security',
label: 'Security',
heading: (
<>
Trusted for{' '}
<span className="text-foreground">financial solutions and transactions of all types</span>
</>
),
subheading:
"Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers' data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.",
features: [
{ icon: ShieldCheck, heading: 'SOC 2 Type II certified' },
{ icon: HeartPulse, heading: 'HIPAA compliant' },
{ icon: ShieldAlert, heading: 'DDoS Protection' },
{ icon: Lock, heading: 'Multi-factor Authentication' },
{ icon: ClipboardCheck, heading: 'Vulnerability Management' },
{ icon: Users, heading: 'Role-based access control' },
{ icon: List, heading: 'Database Audit Logs' },
{ icon: Lightbulb, heading: 'Security Advisors' },
{ icon: FolderLock, heading: 'Encrypted Storage' },
{ icon: UserX, heading: 'Network restrictions' },
],
cta: {
label: 'Learn about security',
url: '/security',
},
},
platformStarterSection: {
id: 'quickstarts',
heading: (
<>
<span className="text-foreground">Choose your platform</span> to start building in seconds
</>
),
headingRight: (
<>
Or, start with <span className="text-foreground">Supabase AI Prompts</span>
</>
),
docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts',
leftFooter: (
<div className="grid grid-cols-5 divide-x divide-y rounded-lg overflow-hidden border">
{frameworks.map((framework) => (
<FrameworkLink key={framework.name} framework={framework} />
))}
</div>
),
aiPrompts: [
{
id: 'auth-setup',
title: 'Bootstrap Next.js app with Supabase Auth',
code: 'Set up Supabase Auth with Next.js: install @supabase/supabase-js and @supabase/ssr, configure environment variables, create browser and server clients, and add middleware for token refresh.',
language: 'markdown',
docsUrl:
'https://supabase.com/docs/guides/getting-started/ai-prompts/nextjs-supabase-auth',
},
{
id: 'rls-policies',
title: 'Create RLS policies',
code: 'Generate Row Level Security policies for secure access control. Retrieve your schema, then write policies that restrict access to sensitive data based on user roles and auth.uid().',
language: 'markdown',
docsUrl:
'https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies',
},
],
},
customerEvidence: {
id: 'case-studies',
heading: (
<>
Customer <span className="text-foreground">stories</span>
</>
),
customers: [
{
name: 'Deriv',
logo: '/images/customers/logos/deriv.png',
highlights: [
'Accelerating online trading with scalable Postgres',
'Automation, integrations, and ecosystem',
],
cta: { label: 'Read the case study', href: '/customers/deriv' },
},
{
name: 'Bree',
logo: '/images/customers/logos/bree.png',
highlights: [
'10X performance gains when switching from Fauna',
'Greater developer velocity and AI-ready foundation',
],
cta: { label: 'Read the case study', href: '/customers/bree' },
},
{
name: 'Next Door Lending',
logo: '/images/customers/logos/next-door-lending.png',
highlights: [
'Top 10 mortgage broker with Supabase',
'Leveraged Postgres for lending workflows',
],
cta: { label: 'Read the case study', href: '/customers/next-door-lending' },
},
{
name: 'Rally',
logo: '/images/customers/logos/rally.png',
highlights: ['Building financial applications on Supabase'],
cta: { label: 'Read the case study', href: '/customers/rally' },
},
{
name: 'Xendit',
logo: '/images/customers/logos/xendit.png',
highlights: [
'Full solution shipped to production in less than one week',
'Payment processor with transaction verification',
],
cta: { label: 'Read the case study', href: '/customers/xendit' },
},
],
},
}
}
export default data
+5 -6
View File
@@ -456,7 +456,7 @@ const data = {
title: 'Foreign Data Wrappers',
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z',
subheading:
'Connect Supabase to Redshift, BigQuery, MySQL, and external APIs for seamless integrations.',
'Connect Supabase to BigQuery, Snowflake, ClickHouse, S3, Stripe, Firebase, and external APIs for seamless integrations.',
image: (
<Image
draggable={false}
@@ -585,10 +585,10 @@ const data = {
},
{
id: 'multi-region',
title: 'Multi-region Deployments',
title: 'Multi-region Options',
description: (
<>
Deploy databases across multiple regions for{' '}
Deploy in your chosen region with optional read replicas in other regions for{' '}
<span className="text-foreground">global availability</span>.
</>
),
@@ -600,9 +600,8 @@ const data = {
title: 'High Availability Architecture',
description: (
<>
Enterprise plans offer{' '}
<span className="text-foreground">automatic failover and redundancy</span> for
mission-critical applications.
Enterprise plans offer <span className="text-foreground">failover and redundancy</span>{' '}
for mission-critical applications.
</>
),
icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557',
+4 -3
View File
@@ -148,7 +148,7 @@ const data: () => {
icon: Sparkles,
heading: 'Speed and AI-powered innovation',
subheading:
'Supabase is just Postgres, so it&apos;s easy to scale your Hackathon project into a real product. Connect to your favorite AI tools and use built-in Vectors to store embeddings in Postgres. Use Foreign Data Wrappers to connect to Google Sheets, MySQL, BigQuery, and more.',
'Supabase is just Postgres, so it&apos;s easy to scale your Hackathon project into a real product. Connect to your favorite AI tools and use built-in Vectors to store embeddings in Postgres. Use Foreign Data Wrappers to connect to BigQuery, Snowflake, S3, Stripe, and more.',
},
],
},
@@ -486,8 +486,9 @@ const data: () => {
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z',
subheading: (
<>
Connect Supabase to <span className="text-foreground">Redshift, BigQuery, MySQL</span>
, and external APIs for seamless integrations.
Connect Supabase to{' '}
<span className="text-foreground">BigQuery, Snowflake, ClickHouse</span>, and external
APIs for seamless integrations.
</>
),
image: (
+301
View File
@@ -0,0 +1,301 @@
import {
ClipboardCheck,
FolderLock,
HeartPulse,
Lightbulb,
List,
Lock,
ShieldAlert,
ShieldCheck,
Users,
UserX,
} from 'lucide-react'
import { TwoColumnsSectionProps } from '~/components/Solutions/TwoColumnsSection'
import { frameworks } from 'components/Hero/HeroFrameworks'
import type { FeatureGridProps } from 'components/Solutions/FeatureGrid'
import type { PlatformSectionProps } from 'components/Solutions/PlatformSection'
import type { ResultsSectionProps } from 'components/Solutions/ResultsSection'
import type { SecuritySectionProps } from 'components/Enterprise/Security'
import type { WhatItTakesSectionProps } from 'components/Solutions/WhatItTakesSection'
import {
FrameworkLink,
type FeaturesSection,
type HeroSection,
type Metadata,
} from './solutions.utils'
import { getSharedSections } from './shared-sections'
import { Image } from 'ui'
import { useSendTelemetryEvent } from 'lib/telemetry'
const data: () => {
metadata: Metadata
heroSection: HeroSection
singleQuote: {
id: string
quote: {
text: string
author: string
role: string
link?: string
logo?: React.ReactElement
}
}
why: FeaturesSection
platform: PlatformSectionProps
developerExperience: ReturnType<typeof getSharedSections>['developerExperience']
resultsSection: ResultsSectionProps
featureGrid: FeatureGridProps
securitySection: SecuritySectionProps
platformStarterSection: TwoColumnsSectionProps
customerEvidence: {
id: string
heading: React.ReactNode
customers: Array<{
name: string
logo?: string
highlights: string[]
cta?: { label: string; href: string }
}>
}
whatItTakes: WhatItTakesSectionProps
} = () => {
const shared = getSharedSections()
const sendTelemetryEvent = useSendTelemetryEvent()
return {
metadata: {
metaTitle: 'Supabase for Healthcare',
metaDescription:
'HIPAA-compliant from day one. Build patient-facing applications, clinical tools, and health data platforms with compliance built in.',
},
heroSection: {
id: 'hero',
title: 'Supabase for Healthcare',
h1: (
<>
<span className="block text-foreground">HIPAA-compliant from day one.</span>
<span className="block md:ml-0">Build with confidence.</span>
</>
),
subheader: [
<>
Supabase provides a fully managed, HIPAA-compliant Postgres platform with PHI protection,
a signed BAA, and audit-ready infrastructure. Build patient-facing applications, clinical
tools, and health data platforms with compliance built in.
</>,
],
image: undefined,
ctas: [
{
label: 'Start your project',
href: 'https://supabase.com/dashboard',
type: 'primary' as any,
onClick: () =>
sendTelemetryEvent({
action: 'start_project_button_clicked',
properties: { buttonLocation: 'Solutions: Healthcare page hero' },
}),
},
{
label: 'Request a demo',
href: 'https://supabase.com/contact/sales',
type: 'default' as any,
onClick: () =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'Solutions: Healthcare page hero' },
}),
},
],
},
singleQuote: {
id: 'social-proof',
quote: {
text: '"For me, the biggest benefit of Supabase is developer experience. My expertise doesn\'t lie in databases and infrastructure."',
author: 'Nick Farrant',
role: 'Founding Engineer, Juniver',
link: '/customers/juniver',
logo: (
<>
<Image
src="/images/customers/logos/juniver.png"
alt="Juniver"
width={128}
height={48}
className="object-contain w-24 md:w-32 dark:hidden"
/>
<Image
src="/images/customers/logos/light/juniver.png"
alt="Juniver"
width={128}
height={48}
className="object-contain w-24 md:w-32 hidden dark:block"
/>
</>
),
},
},
why: {
id: 'why-supabase',
label: '',
heading: (
<>
Why <span className="text-foreground">healthcare companies</span> choose Supabase
</>
),
subheading:
'Build secure, scalable healthcare applications using a trusted, HIPAA-compliant data platform.',
features: [
{
id: 'hipaa-baa',
icon: HeartPulse,
heading: 'HIPAA-compliant with a signed BAA.',
subheading:
'Enable the HIPAA add-on, sign a Business Associate Agreement, and store Protected Health Information on infrastructure that meets the Security Rule. Not a checkbox exercise. Real technical and administrative safeguards.',
},
{
id: 'audit-trails',
icon: List,
heading: 'Audit trails that satisfy regulators.',
subheading:
'Every data access, every modification, every login is logged. Database audit logs and Row Level Security give you the documentation regulators expect during compliance reviews.',
},
{
id: 'patient-data',
icon: Lock,
heading: 'Patient data stays where it belongs.',
subheading:
'Encrypt at rest and in transit, enforce role-based access controls, and use RLS to guarantee that each provider, clinic, or patient only sees their own data. Multi-region deployment options support data residency requirements.',
},
],
},
platform: {
...shared.platform,
title: (
<>
Supabase is the <span className="text-foreground">HIPAA-compliant Postgres platform</span>{' '}
you control
</>
),
subheading:
'Supabase includes everything you need to build HIPAA-compliant healthcare applications.',
},
developerExperience: shared.developerExperience,
resultsSection: shared.resultsSection,
featureGrid: shared.featureGrid,
securitySection: {
id: 'security',
label: 'Security',
heading: (
<>
Trusted for{' '}
<span className="text-foreground">medical records and health data of all types</span>
</>
),
subheading:
"Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers' data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.",
features: [
{ icon: ShieldCheck, heading: 'SOC 2 Type II certified' },
{ icon: HeartPulse, heading: 'HIPAA compliant' },
{ icon: ShieldAlert, heading: 'DDoS Protection' },
{ icon: Lock, heading: 'Multi-factor Authentication' },
{ icon: ClipboardCheck, heading: 'Vulnerability Management' },
{ icon: Users, heading: 'Role-based access control' },
{ icon: List, heading: 'Database Audit Logs' },
{ icon: Lightbulb, heading: 'Security Advisors' },
{ icon: FolderLock, heading: 'Encrypted Storage' },
{ icon: UserX, heading: 'Network restrictions' },
],
cta: {
label: 'Learn about security',
url: '/security',
},
},
platformStarterSection: {
id: 'quickstarts',
heading: (
<>
<span className="text-foreground">Choose your platform</span> to start building in seconds
</>
),
headingRight: (
<>
Or, start with <span className="text-foreground">Supabase AI Prompts</span>
</>
),
docsUrl: 'https://supabase.com/docs/guides/getting-started/ai-prompts',
leftFooter: (
<div className="grid grid-cols-5 divide-x divide-y rounded-lg overflow-hidden border">
{frameworks.map((framework) => (
<FrameworkLink key={framework.name} framework={framework} />
))}
</div>
),
aiPrompts: [
{
id: 'auth-setup',
title: 'Bootstrap Next.js app with Supabase Auth',
code: 'Set up Supabase Auth with Next.js: install @supabase/supabase-js and @supabase/ssr, configure environment variables, create browser and server clients, and add middleware for token refresh.',
language: 'markdown',
docsUrl:
'https://supabase.com/docs/guides/getting-started/ai-prompts/nextjs-supabase-auth',
},
{
id: 'rls-policies',
title: 'Create RLS policies',
code: 'Generate Row Level Security policies for healthcare data. Retrieve your schema, then write policies that restrict access to PHI based on user roles, auth.uid(), and organizational boundaries.',
language: 'markdown',
docsUrl:
'https://supabase.com/docs/guides/getting-started/ai-prompts/database-rls-policies',
},
],
},
customerEvidence: {
id: 'case-studies',
heading: (
<>
Customer <span className="text-foreground">stories</span>
</>
),
customers: [
{
name: 'Juniver',
logo: '/images/customers/logos/juniver.png',
highlights: [
'Automated B2B workflows with Edge Functions and RLS',
'Improved developer experience and performance',
],
cta: { label: 'Read the case study', href: '/customers/juniver' },
},
],
},
whatItTakes: {
id: 'what-it-takes',
heading: (
<>
What it takes <span className="text-foreground">to be HIPAA-compliant</span> on Supabase
</>
),
items: [
{
id: 'hipaa-guide',
description:
'Read our guide on HIPAA and learn more about our shared responsibilities in delivering compliant solutions.',
url: '/docs/guides/security/hipaa-compliance',
linkLabel: 'Read more',
},
{
id: 'enable-hipaa',
description: 'Enable HIPAA compliance in your project.',
url: '/docs/guides/platform/hipaa-projects',
linkLabel: 'Read more',
},
],
},
}
}
export default data
+4 -4
View File
@@ -695,10 +695,10 @@ const data: () => {
},
{
id: 'multi-region',
title: 'Multi-region Deployments',
title: 'Multi-region Options',
description: (
<>
Deploy databases across multiple regions for{' '}
Deploy in your chosen region with optional read replicas in other regions for{' '}
<span className="text-foreground">global availability</span>.
</>
),
@@ -711,8 +711,8 @@ const data: () => {
description: (
<>
Enterprise plans offer{' '}
<span className="text-foreground">automatic failover and redundancy</span> for
mission-critical applications.
<span className="text-foreground">failover and redundancy</span> for mission-critical
applications.
</>
),
icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557',
+5 -6
View File
@@ -448,7 +448,7 @@ const data = {
title: 'Foreign Data Wrappers',
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z',
subheading:
'Connect Supabase to Redshift, BigQuery, MySQL, and external APIs for seamless integrations.',
'Connect Supabase to BigQuery, Snowflake, ClickHouse, S3, Stripe, Firebase, and external APIs for seamless integrations.',
image: (
<Image
draggable={false}
@@ -577,10 +577,10 @@ const data = {
},
{
id: 'multi-region',
title: 'Multi-region Deployments',
title: 'Multi-region Options',
description: (
<>
Deploy databases across multiple regions for{' '}
Deploy in your chosen region with optional read replicas in other regions for{' '}
<span className="text-foreground">global availability</span>.
</>
),
@@ -592,9 +592,8 @@ const data = {
title: 'High Availability Architecture',
description: (
<>
Enterprise plans offer{' '}
<span className="text-foreground">automatic failover and redundancy</span> for
mission-critical applications.
Enterprise plans offer <span className="text-foreground">failover and redundancy</span>{' '}
for mission-critical applications.
</>
),
icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557',
@@ -597,8 +597,9 @@ const data: () => {
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z',
subheading: (
<>
Connect Supabase to <span className="text-foreground">Redshift, BigQuery, MySQL</span>
, and external APIs for seamless integrations.
Connect Supabase to{' '}
<span className="text-foreground">BigQuery, Snowflake, ClickHouse</span>, S3, Stripe,
Firebase, and external APIs for seamless integrations.
</>
),
image: (
@@ -733,11 +734,12 @@ const data: () => {
},
{
id: 'multi-region',
title: 'Multi-region Deployments',
title: 'Multi-region Options',
description: (
<>
Deploy databases across multiple regions for{' '}
<span className="text-foreground">global availability</span>.
Deploy in your chosen region with optional{' '}
<span className="text-foreground">read replicas</span> in other regions for global
availability.
</>
),
icon: 'M8.48462 3.05339C6.79298 3.58819 5.33457 4.64831 4.30037 6.0436C3.4029 7.25444 2.82613 8.71636 2.69516 10.306H6.77142C6.83771 8.01994 7.22916 5.93809 7.84745 4.36313C8.03485 3.88578 8.24723 3.44433 8.48462 3.05339ZM10.9999 1.27832C7.79633 1.27832 4.95467 2.82842 3.18457 5.21656C1.98658 6.83284 1.2778 8.83471 1.2778 11.0001C1.2778 13.1781 1.99476 15.1906 3.20527 16.8117C4.97675 19.1842 7.80877 20.7225 10.9999 20.7225C14.191 20.7225 17.023 19.1841 18.7944 16.8117C20.005 15.1906 20.722 13.1781 20.722 11.0001C20.722 8.83471 20.0132 6.83284 18.8152 5.21656L18.7944 5.18864C17.0229 2.81635 14.1909 1.27832 10.9999 1.27832ZM10.9999 2.66721C10.768 2.66721 10.4732 2.78413 10.1294 3.15462C9.78466 3.52602 9.44227 4.10142 9.14028 4.87067C8.596 6.2571 8.22699 8.16013 8.16092 10.306H13.8389C13.7728 8.16013 13.4038 6.2571 12.8595 4.87067C12.5575 4.10142 12.2151 3.52602 11.8704 3.15462C11.5265 2.78413 11.2318 2.66721 10.9999 2.66721ZM15.2284 10.306C15.1621 8.01994 14.7706 5.93809 14.1523 4.36313C13.9649 3.88578 13.7525 3.44433 13.5152 3.05339C15.1971 3.58512 16.6485 4.63618 17.6816 6.01966L17.6994 6.0436C18.5969 7.25443 19.1737 8.71636 19.3046 10.306H15.2284ZM13.8389 11.6949H8.16092C8.22699 13.8407 8.596 15.7437 9.14028 17.1301C9.44227 17.8994 9.78466 18.4748 10.1294 18.8462C10.4732 19.2167 10.768 19.3336 10.9999 19.3336C11.2318 19.3336 11.5265 19.2167 11.8704 18.8462C12.2151 18.4748 12.5575 17.8994 12.8595 17.1301C13.4038 15.7437 13.7728 13.8407 13.8389 11.6949ZM13.5152 18.9473C13.7526 18.5564 13.965 18.115 14.1523 17.6377C14.7706 16.0627 15.1621 13.9809 15.2284 11.6949H19.3046C19.1727 13.2947 18.5892 14.7653 17.6816 15.9807C16.6485 17.3643 15.1971 18.4155 13.5152 18.9473ZM8.48458 18.9474C8.24721 18.5564 8.03484 18.115 7.84745 17.6377C7.22916 16.0627 6.83771 13.9809 6.77142 11.6949H2.6952C2.82712 13.2947 3.41061 14.7653 4.31815 15.9808C5.35126 17.3644 6.80264 18.4156 8.48458 18.9474Z',
@@ -748,9 +750,9 @@ const data: () => {
title: 'High Availability Architecture',
description: (
<>
Enterprise plans offer{' '}
<span className="text-foreground">automatic failover and redundancy</span> for
mission-critical applications.
Enterprise plans include{' '}
<span className="text-foreground">failover and redundancy</span> for mission-critical
applications.
</>
),
icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557',
@@ -0,0 +1,15 @@
/**
* Shared sections reused from postgres-developers across app-type solution pages
* (B2B SaaS, FinServ, Healthcare, Agents)
*/
import getPostgresDevelopersContent from './postgres-developers'
export const getSharedSections = () => {
const pg = getPostgresDevelopersContent()
return {
platform: pg.platform,
developerExperience: pg.developerExperience,
resultsSection: pg.resultsSection,
featureGrid: pg.featureGrid,
}
}
+7 -6
View File
@@ -486,8 +486,9 @@ const data: () => {
icon: 'M10.2805 18.2121C11.2419 18.6711 12.3325 18.8932 13.4711 18.8084C15.2257 18.6776 16.7596 17.843 17.8169 16.6015M8.21496 8.36469C9.27117 7.14237 10.7928 6.322 12.5311 6.19248C13.7196 6.10392 14.8558 6.34979 15.8474 6.85054M17.8169 16.6015L20.5242 19.3223C22.1857 17.5141 23.1562 15.1497 23.1562 12.5005C23.1562 6.89135 18.6091 2.34424 13 2.34424C10.9595 2.34424 9.16199 2.87659 7.57035 3.91232C8.35717 3.56865 9.22613 3.37801 10.1396 3.37801C12.6236 3.37801 14.7783 4.78762 15.8474 6.85054M17.8169 16.6015V16.6015C16.277 15.059 16.3448 12.5527 16.5387 10.3817C16.5557 10.191 16.5644 9.99794 16.5644 9.80282C16.5644 8.73844 16.3056 7.73451 15.8474 6.85054M13 22.6567C7.39086 22.6567 2.84375 18.1096 2.84375 12.5005C2.84375 9.84123 3.8026 7.48969 5.4753 5.67921L8.21496 8.42354V8.42354C9.76942 9.98064 9.69844 12.5133 9.51947 14.7062C9.50526 14.8803 9.49802 15.0564 9.49802 15.2341C9.49802 18.7705 12.3648 21.6373 15.9012 21.6373C16.8116 21.6373 17.6776 21.4473 18.4618 21.1048C16.8609 22.1588 15.06 22.6567 13 22.6567Z',
subheading: (
<>
Connect Supabase to <span className="text-foreground">Redshift, BigQuery, MySQL</span>
, and external APIs for seamless integrations.
Connect Supabase to{' '}
<span className="text-foreground">BigQuery, Snowflake, ClickHouse</span>, and external
APIs for seamless integrations.
</>
),
image: (
@@ -622,10 +623,10 @@ const data: () => {
},
{
id: 'multi-region',
title: 'Multi-region Deployments',
title: 'Multi-region Options',
description: (
<>
Deploy databases across multiple regions for{' '}
Deploy in your chosen region with optional read replicas in other regions for{' '}
<span className="text-foreground">global availability</span>.
</>
),
@@ -638,8 +639,8 @@ const data: () => {
description: (
<>
Enterprise plans offer{' '}
<span className="text-foreground">automatic failover and redundancy</span> for
mission-critical applications.
<span className="text-foreground">failover and redundancy</span> for mission-critical
applications.
</>
),
icon: 'M16.3046 3.24514C15.3004 2.91279 14.2268 2.73291 13.1111 2.73291C7.50197 2.73291 2.95486 7.28002 2.95486 12.8892C2.95486 18.4983 7.50197 23.0454 13.1111 23.0454C18.7203 23.0454 23.2674 18.4983 23.2674 12.8892C23.2674 10.5703 22.4902 8.4329 21.1822 6.72328L12.2253 15.5572L10.2303 13.5622M13.2175 6.31682C9.54013 6.31682 6.55899 9.29795 6.55899 12.4809C6.55899 16.1583 9.54013 19.1395 13.2175 19.1395C16.895 19.1395 19.8761 16.1583 19.8761 12.4809C19.8761 11.1095 19.4615 9.83483 18.7507 8.77557',
+88
View File
@@ -0,0 +1,88 @@
import { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import Layout from 'components/Layouts/Default'
import ProductHeader from 'components/Sections/ProductHeader2'
import SolutionsStickyNav from 'components/SolutionsStickyNav'
import getContent from 'data/solutions/agents'
import { Solutions } from 'data/Solutions'
const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection'))
const SingleQuote = dynamic(() => import('components/Sections/SingleQuote'))
const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection'))
const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection'))
const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection'))
const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid'))
const Security = dynamic(() => import('components/Enterprise/Security'))
const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection'))
const CustomerEvidenceSection = dynamic(
() => import('components/Solutions/CustomerEvidenceSection')
)
const AgentsPage: NextPage = () => {
const content = getContent()
return (
<>
<NextSeo
title={content.metadata.metaTitle}
description={content.metadata.metaDescription}
openGraph={{
title: content.metadata.metaTitle,
description: content.metadata.metaDescription,
url: `https://supabase.com/solutions/agents`,
}}
/>
<Layout className="overflow-visible">
<SolutionsStickyNav activeItem={Solutions.agents} type="app-type" />
<ProductHeader
{...content.heroSection}
className="[&_h1]:2xl:!text-5xl bg-default border-0 lg:pb-8 [&_.ph-footer]:mt-0 [&_.ph-footer]:lg:mt-16 [&_.ph-footer]:xl:mt-32"
sectionContainerClassName="lg:gap-4"
/>
<SingleQuote
id={content.singleQuote.id}
quote={{
text: content.singleQuote.quote.text,
author: content.singleQuote.quote.author,
role: content.singleQuote.quote.role,
link: content.singleQuote.quote.link,
logo: content.singleQuote.quote.logo,
}}
className="!pb-8 md:!pb-12 [&_q]:max-w-2xl"
/>
<WhySupabase {...content.why} />
<PlatformSection {...content.platform} />
<DXSection
id={content.developerExperience.id}
title={content.developerExperience.title}
subheading={content.developerExperience.subheading}
features={content.developerExperience.features}
className={content.developerExperience.className}
/>
<ResultsSection
id={content.resultsSection.id}
heading={content.resultsSection.heading}
subheading={content.resultsSection.subheading}
highlights={content.resultsSection.highlights}
/>
<FeatureGrid id={content.featureGrid.id} features={content.featureGrid.features} />
<Security
id={content.securitySection.id}
label={content.securitySection.label}
heading={content.securitySection.heading}
subheading={content.securitySection.subheading}
features={content.securitySection.features}
cta={content.securitySection.cta}
/>
<WhySupabase {...content.ecosystemSection} />
<PlatformStarterSection {...content.platformStarterSection} />
<CustomerEvidenceSection {...content.customerEvidence} />
</Layout>
</>
)
}
export default AgentsPage
+87
View File
@@ -0,0 +1,87 @@
import { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import Layout from 'components/Layouts/Default'
import ProductHeader from 'components/Sections/ProductHeader2'
import SolutionsStickyNav from 'components/SolutionsStickyNav'
import getContent from 'data/solutions/b2b-saas'
import { Solutions } from 'data/Solutions'
const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection'))
const SingleQuote = dynamic(() => import('components/Sections/SingleQuote'))
const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection'))
const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection'))
const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection'))
const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid'))
const Security = dynamic(() => import('components/Enterprise/Security'))
const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection'))
const CustomerEvidenceSection = dynamic(
() => import('components/Solutions/CustomerEvidenceSection')
)
const B2BSaaSPage: NextPage = () => {
const content = getContent()
return (
<>
<NextSeo
title={content.metadata.metaTitle}
description={content.metadata.metaDescription}
openGraph={{
title: content.metadata.metaTitle,
description: content.metadata.metaDescription,
url: `https://supabase.com/solutions/b2b-saas`,
}}
/>
<Layout className="overflow-visible">
<SolutionsStickyNav activeItem={Solutions.b2bSaaS} type="app-type" />
<ProductHeader
{...content.heroSection}
className="[&_h1]:2xl:!text-5xl bg-default border-0 lg:pb-8 [&_.ph-footer]:mt-0 [&_.ph-footer]:lg:mt-16 [&_.ph-footer]:xl:mt-32"
sectionContainerClassName="lg:gap-4"
/>
<SingleQuote
id={content.singleQuote.id}
quote={{
text: content.singleQuote.quote.text,
author: content.singleQuote.quote.author,
role: content.singleQuote.quote.role,
link: content.singleQuote.quote.link,
logo: content.singleQuote.quote.logo,
}}
className="!pb-8 md:!pb-12 [&_q]:max-w-2xl"
/>
<WhySupabase {...content.why} />
<PlatformSection {...content.platform} />
<DXSection
id={content.developerExperience.id}
title={content.developerExperience.title}
subheading={content.developerExperience.subheading}
features={content.developerExperience.features}
className={content.developerExperience.className}
/>
<ResultsSection
id={content.resultsSection.id}
heading={content.resultsSection.heading}
subheading={content.resultsSection.subheading}
highlights={content.resultsSection.highlights}
/>
<FeatureGrid id={content.featureGrid.id} features={content.featureGrid.features} />
<Security
id={content.securitySection.id}
label={content.securitySection.label}
heading={content.securitySection.heading}
subheading={content.securitySection.subheading}
features={content.securitySection.features}
cta={content.securitySection.cta}
/>
<PlatformStarterSection {...content.platformStarterSection} />
<CustomerEvidenceSection {...content.customerEvidence} />
</Layout>
</>
)
}
export default B2BSaaSPage
+87
View File
@@ -0,0 +1,87 @@
import { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import Layout from 'components/Layouts/Default'
import ProductHeader from 'components/Sections/ProductHeader2'
import SolutionsStickyNav from 'components/SolutionsStickyNav'
import getContent from 'data/solutions/finserv'
import { Solutions } from 'data/Solutions'
const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection'))
const SingleQuote = dynamic(() => import('components/Sections/SingleQuote'))
const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection'))
const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection'))
const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection'))
const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid'))
const Security = dynamic(() => import('components/Enterprise/Security'))
const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection'))
const CustomerEvidenceSection = dynamic(
() => import('components/Solutions/CustomerEvidenceSection')
)
const FinServPage: NextPage = () => {
const content = getContent()
return (
<>
<NextSeo
title={content.metadata.metaTitle}
description={content.metadata.metaDescription}
openGraph={{
title: content.metadata.metaTitle,
description: content.metadata.metaDescription,
url: `https://supabase.com/solutions/finserv`,
}}
/>
<Layout className="overflow-visible">
<SolutionsStickyNav activeItem={Solutions.finserv} type="app-type" />
<ProductHeader
{...content.heroSection}
className="[&_h1]:2xl:!text-5xl bg-default border-0 lg:pb-8 [&_.ph-footer]:mt-0 [&_.ph-footer]:lg:mt-16 [&_.ph-footer]:xl:mt-32"
sectionContainerClassName="lg:gap-4"
/>
<SingleQuote
id={content.singleQuote.id}
quote={{
text: content.singleQuote.quote.text,
author: content.singleQuote.quote.author,
role: content.singleQuote.quote.role,
link: content.singleQuote.quote.link,
logo: content.singleQuote.quote.logo,
}}
className="!pb-8 md:!pb-12 [&_q]:max-w-2xl"
/>
<WhySupabase {...content.why} />
<PlatformSection {...content.platform} />
<DXSection
id={content.developerExperience.id}
title={content.developerExperience.title}
subheading={content.developerExperience.subheading}
features={content.developerExperience.features}
className={content.developerExperience.className}
/>
<ResultsSection
id={content.resultsSection.id}
heading={content.resultsSection.heading}
subheading={content.resultsSection.subheading}
highlights={content.resultsSection.highlights}
/>
<FeatureGrid id={content.featureGrid.id} features={content.featureGrid.features} />
<Security
id={content.securitySection.id}
label={content.securitySection.label}
heading={content.securitySection.heading}
subheading={content.securitySection.subheading}
features={content.securitySection.features}
cta={content.securitySection.cta}
/>
<PlatformStarterSection {...content.platformStarterSection} />
<CustomerEvidenceSection {...content.customerEvidence} />
</Layout>
</>
)
}
export default FinServPage
+89
View File
@@ -0,0 +1,89 @@
import { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import Layout from 'components/Layouts/Default'
import ProductHeader from 'components/Sections/ProductHeader2'
import SolutionsStickyNav from 'components/SolutionsStickyNav'
import getContent from 'data/solutions/healthcare'
import { Solutions } from 'data/Solutions'
const WhySupabase = dynamic(() => import('components/Solutions/FeaturesSection'))
const SingleQuote = dynamic(() => import('components/Sections/SingleQuote'))
const PlatformSection = dynamic(() => import('components/Solutions/PlatformSection'))
const DXSection = dynamic(() => import('components/Solutions/DeveloperExperienceSection'))
const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection'))
const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid'))
const Security = dynamic(() => import('components/Enterprise/Security'))
const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection'))
const CustomerEvidenceSection = dynamic(
() => import('components/Solutions/CustomerEvidenceSection')
)
const WhatItTakesSection = dynamic(() => import('components/Solutions/WhatItTakesSection'))
const HealthcarePage: NextPage = () => {
const content = getContent()
return (
<>
<NextSeo
title={content.metadata.metaTitle}
description={content.metadata.metaDescription}
openGraph={{
title: content.metadata.metaTitle,
description: content.metadata.metaDescription,
url: `https://supabase.com/solutions/healthcare`,
}}
/>
<Layout className="overflow-visible">
<SolutionsStickyNav activeItem={Solutions.healthcare} type="app-type" />
<ProductHeader
{...content.heroSection}
className="[&_h1]:2xl:!text-5xl bg-default border-0 lg:pb-8 [&_.ph-footer]:mt-0 [&_.ph-footer]:lg:mt-16 [&_.ph-footer]:xl:mt-32"
sectionContainerClassName="lg:gap-4"
/>
<SingleQuote
id={content.singleQuote.id}
quote={{
text: content.singleQuote.quote.text,
author: content.singleQuote.quote.author,
role: content.singleQuote.quote.role,
link: content.singleQuote.quote.link,
logo: content.singleQuote.quote.logo,
}}
className="!pb-8 md:!pb-12 [&_q]:max-w-2xl"
/>
<WhySupabase {...content.why} />
<PlatformSection {...content.platform} />
<DXSection
id={content.developerExperience.id}
title={content.developerExperience.title}
subheading={content.developerExperience.subheading}
features={content.developerExperience.features}
className={content.developerExperience.className}
/>
<ResultsSection
id={content.resultsSection.id}
heading={content.resultsSection.heading}
subheading={content.resultsSection.subheading}
highlights={content.resultsSection.highlights}
/>
<FeatureGrid id={content.featureGrid.id} features={content.featureGrid.features} />
<Security
id={content.securitySection.id}
label={content.securitySection.label}
heading={content.securitySection.heading}
subheading={content.securitySection.subheading}
features={content.securitySection.features}
cta={content.securitySection.cta}
/>
<PlatformStarterSection {...content.platformStarterSection} />
<CustomerEvidenceSection {...content.customerEvidence} />
<WhatItTakesSection {...content.whatItTakes} />
</Layout>
</>
)
}
export default HealthcarePage
+5 -61
View File
@@ -2,7 +2,7 @@ import { NextPage } from 'next'
import dynamic from 'next/dynamic'
import { NextSeo } from 'next-seo'
import Link from 'next/link'
import { Check, ArrowRight } from 'lucide-react'
import { Check } from 'lucide-react'
import Layout from 'components/Layouts/Default'
import SolutionsStickyNav from 'components/SolutionsStickyNav'
@@ -19,6 +19,9 @@ const DXSection = dynamic(() => import('components/Solutions/DeveloperExperience
const ResultsSection = dynamic(() => import('components/Solutions/ResultsSection'))
const FeatureGrid = dynamic(() => import('components/Solutions/FeatureGrid'))
const PlatformStarterSection = dynamic(() => import('components/Solutions/TwoColumnsSection'))
const CustomerEvidenceSection = dynamic(
() => import('components/Solutions/CustomerEvidenceSection')
)
const MPCSection = dynamic(() => import('components/Solutions/MPCSection'))
const SecuritySection = dynamic(() => import('components/Enterprise/Security'))
@@ -193,66 +196,7 @@ const InnovationTeams: NextPage = () => {
subheading={data.why.subheading}
features={data.why.features}
/>
{/* Customer Evidence Section */}
{data.customerEvidence && (
<SectionContainer id={data.customerEvidence.id} className="py-16 md:py-24">
<div className="text-center max-w-3xl mx-auto mb-12">
<h2 className="text-2xl sm:text-3xl lg:text-4xl text-foreground-lighter">
{data.customerEvidence.heading}
</h2>
</div>
<div className="grid md:grid-cols-2 gap-0 max-w-5xl mx-auto rounded-lg border overflow-hidden divide-x">
{data.customerEvidence.customers.map(
(customer: {
name: string
logo?: string
highlights: string[]
cta?: { label: string; href: string }
}) => (
<div key={customer.name} className="p-6 md:p-8 flex flex-col">
{customer.logo && (
<div
className="h-8 w-28 mb-6"
style={{
maskImage: `url(${customer.logo})`,
WebkitMaskImage: `url(${customer.logo})`,
maskSize: 'contain',
WebkitMaskSize: 'contain',
maskRepeat: 'no-repeat',
WebkitMaskRepeat: 'no-repeat',
maskPosition: 'left center',
WebkitMaskPosition: 'left center',
backgroundColor: 'currentColor',
}}
/>
)}
<h3 className="text-lg text-foreground font-medium mb-4">{customer.name}</h3>
<ul className="space-y-3 mb-6 flex-grow">
{customer.highlights.map((highlight: string, i: number) => (
<li
key={i}
className="flex items-start gap-3 text-foreground-lighter text-sm"
>
<Check className="w-4 h-4 text-brand shrink-0 mt-0.5" />
<span>{highlight}</span>
</li>
))}
</ul>
{customer.cta && (
<Link
href={customer.cta.href}
className="inline-flex items-center gap-2 text-sm text-brand hover:text-brand-600 transition-colors"
>
{customer.cta.label}
<ArrowRight className="w-4 h-4" />
</Link>
)}
</div>
)
)}
</div>
</SectionContainer>
)}
{data.customerEvidence && <CustomerEvidenceSection {...data.customerEvidence} />}
<PlatformSection
id={data.platform.id}
title={data.platform.title}
+19
View File
@@ -267,3 +267,22 @@ KONG_HTTPS_PORT=8443
# Enable webp support
IMGPROXY_ENABLE_WEBP_DETECTION=true
############
# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt
############
# Documentation:
# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https
# Usage:
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
# Domain name for the proxy (must point to your server)
PROXY_DOMAIN=your-domain.example.com
# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN).
# This should be a valid email, not a placehoder (otherwise Certbot may fail to start).
CERTBOT_EMAIL=admin@example.com
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more