diff --git a/apps/docs/components/MDX/auth_helpers_faq.mdx b/apps/docs/components/MDX/auth_helpers_faq.mdx
new file mode 100644
index 00000000000..0f9a289d784
--- /dev/null
+++ b/apps/docs/components/MDX/auth_helpers_faq.mdx
@@ -0,0 +1,31 @@
+### Fetch requests to API endpoints aren't showing the session
+
+You must pass along the cookie header with the fetch request in order for your API endpoint to get access to the cookie from this request.
+
+```ts
+const res = await fetch('http://localhost/contact', {
+ headers: {
+ cookie: headers().get('cookie') as string,
+ },
+})
+```
+
+### Performing administration tasks on the server side with the `service_role` `secret`
+
+By default, the auth-helpers do not permit the use of the `service_role` `secret`. This restriction is in place to prevent the accidental exposure of your `service_role` `secret` to the public. Since the auth-helpers function on both the server and client side, it becomes challenging to separate the key specifically for client-side usage.
+
+However, there is a solution. You can create a separate Supabase client using the `createClient` method from `@supabase/supabase-js` and provide it with the `service_role` `secret`. In a server environment, you will also need to disable certain properties to ensure proper functionality.
+
+By implementing this approach, you can safely utilize the `service_role` `secret` without compromising security or exposing sensitive information to the public.
+
+```ts
+import { createClient } from '@supabase/supabase-js'
+
+const supabase = createClient(supabaseUrl, serviceRoleSecret, {
+ auth: {
+ persistSession: false,
+ autoRefreshToken: false,
+ detectSessionInUrl: false,
+ },
+})
+```
diff --git a/apps/docs/components/index.tsx b/apps/docs/components/index.tsx
index 5a50214c12e..2e999d38a66 100644
--- a/apps/docs/components/index.tsx
+++ b/apps/docs/components/index.tsx
@@ -22,6 +22,7 @@ import QuickstartIntro from './MDX/quickstart_intro.mdx'
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
import StorageManagement from './MDX/storage_management.mdx'
+import AuthHelpersFAQ from './MDX/auth_helpers_faq.mdx'
import { CH } from '@code-hike/mdx/components'
import RefHeaderSection from './reference/RefHeaderSection'
@@ -74,6 +75,7 @@ const components = {
SocialProviderSettingsSupabase,
StepHikeCompact,
StorageManagement,
+ AuthHelpersFAQ,
Mermaid,
Extensions,
Alert: (props: any) => (
diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx
index 0e38a19150a..4e08a013707 100644
--- a/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx
+++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs.mdx
@@ -79,7 +79,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary.
-In Next.js Server Components, you can read a cookie, but you can't write back to it. Middleware on the other hand, allow you to both read a write to cookies.
+Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies.
Next.js [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) runs immediately before each route is rendered. We'll use Middleware to refresh the user's session before loading Server Component routes.
@@ -528,7 +528,7 @@ This allows for the Supabase client to be easily instantiated in the correct con
diff --git a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx
index c1e5151a91d..2f654f39ffa 100644
--- a/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx
+++ b/studio/components/interfaces/Organization/BillingSettingsV2/Subscription/PaymentMethodSelection.tsx
@@ -14,11 +14,13 @@ import { AddNewPaymentMethodModal } from 'components/interfaces/BillingV2'
export interface PaymentMethodSelectionProps {
selectedPaymentMethod?: string
onSelectPaymentMethod: (id: string) => void
+ layout?: 'vertical' | 'horizontal'
}
const PaymentMethodSelection = ({
selectedPaymentMethod,
onSelectPaymentMethod,
+ layout = 'vertical',
}: PaymentMethodSelectionProps) => {
const { ui } = useStore()
const { ref: projectRef } = useParams()
@@ -70,8 +72,7 @@ const PaymentMethodSelection = ({
return (
<>
-
-
Select payment method
+
{isLoading ? (
@@ -91,6 +92,7 @@ const PaymentMethodSelection = ({
disabled={!canUpdatePaymentMethods}
icon={}
onClick={() => setShowAddNewPaymentMethodModal(true)}
+ htmlType='button'
>
Add new
@@ -116,7 +118,13 @@ const PaymentMethodSelection = ({
) : (
-
+
{paymentMethods.map((method: any) => {
const label = `•••• •••• •••• ${method.card.last4}`
return (
diff --git a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx
index fb36c0f1c4b..a08dbc29da0 100644
--- a/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx
+++ b/studio/components/interfaces/Organization/GeneralSettings/MigrateOrganizationBillingButton.tsx
@@ -10,17 +10,18 @@ import { useOrganizationBillingMigrationMutation } from 'data/organizations/orga
import { useOrganizationBillingMigrationPreview } from 'data/organizations/organization-migrate-billing-preview-query'
import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
import { PRICING_TIER_LABELS_ORG } from 'lib/constants'
+import PaymentMethodSelection from '../BillingSettingsV2/Subscription/PaymentMethodSelection'
const MigrateOrganizationBillingButton = observer(() => {
const { ui } = useStore()
const router = useRouter()
-
const organization = useSelectedOrganization()
const [isOpen, setIsOpen] = useState(false)
const [tier, setTier] = useState('')
const [showSpendCapHelperModal, setShowSpendCapHelperModal] = useState(false)
const [isSpendCapEnabled, setIsSpendCapEnabled] = useState(true)
+ const [paymentMethodId, setPaymentMethodId] = useState('')
const dbTier = useMemo(() => {
if (tier === '') return ''
@@ -80,23 +81,15 @@ const MigrateOrganizationBillingButton = observer(() => {
setIsOpen(!isOpen)
}
- const onValidate = (values: any) => {
- const errors: any = {}
- if (!values.tier) {
- errors.tier = 'Please select a plan.'
- }
- return errors
- }
-
- const onConfirmMigrate = async (values: any) => {
+ const onConfirmMigrate = async () => {
+ if (!tier) return
if (!canMigrateOrganization) {
return ui.setNotification({
category: 'error',
message: 'You do not have the required permissions to migrate this organization',
})
}
-
- migrateBilling({ organizationSlug: organization?.slug, tier: dbTier })
+ migrateBilling({ organizationSlug: organization?.slug, tier: dbTier, paymentMethodId })
}
return (
@@ -118,167 +111,174 @@ const MigrateOrganizationBillingButton = observer(() => {
}
>
-
diff --git a/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx b/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx
index f8850e94a82..c8c0a009d21 100644
--- a/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx
+++ b/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx
@@ -15,6 +15,7 @@ import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscr
import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
import Telemetry from 'lib/telemetry'
+import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
import { Alert, Button, IconExternalLink, IconInfo, Modal, Radio, SidePanel } from 'ui'
@@ -106,6 +107,7 @@ const ComputeInstanceSidePanel = () => {
const isSubmitting = isUpdating || isRemoving
const projectId = selectedProject?.id
+ const cpuArchitecture = getCloudProviderArchitecture(selectedProject?.cloud_provider)
const selectedAddons = addons?.selected_addons ?? []
const availableAddons = addons?.available_addons ?? []
@@ -299,7 +301,7 @@ const ComputeInstanceSidePanel = () => {
{option.meta?.memory_gb ?? 0} GB memory
- {option.meta?.cpu_cores ?? 0}-core ARM CPU (
+ {option.meta?.cpu_cores ?? 0}-core {cpuArchitecture} CPU (
{option.meta?.cpu_dedicated ? 'Dedicated' : 'Shared'})
@@ -354,7 +356,7 @@ const ComputeInstanceSidePanel = () => {
{selectedCategory === 'micro' && (
- Your database will use the standard Micro size instance of 2-core ARM CPU (Shared)
+ Your database will use the standard Micro size instance of 2-core {cpuArchitecture} CPU (Shared)
with 1GB of memory.
)}
diff --git a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
index 7230161a181..6792ab6557e 100644
--- a/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
+++ b/studio/components/interfaces/Settings/Database/ConnectionPooling.tsx
@@ -125,7 +125,7 @@ export const PgbouncerConfig: FC
= ({ projectRef, bouncerInfo, conn
const [updates, setUpdates] = useState({
pool_mode: bouncerInfo.pool_mode || 'transaction',
- default_pool_size: bouncerInfo.default_pool_size || '',
+ default_pool_size: bouncerInfo.default_pool_size || undefined,
ignore_startup_parameters: bouncerInfo.ignore_startup_parameters || '',
pgbouncer_enabled: bouncerInfo.pgbouncer_enabled,
})
diff --git a/studio/components/interfaces/Support/SupportForm.tsx b/studio/components/interfaces/Support/SupportForm.tsx
index 3bec0ff1722..851cb3c760d 100644
--- a/studio/components/interfaces/Support/SupportForm.tsx
+++ b/studio/components/interfaces/Support/SupportForm.tsx
@@ -19,8 +19,8 @@ import {
import { useParams } from 'common'
import Divider from 'components/ui/Divider'
import InformationBox from 'components/ui/InformationBox'
-import Connecting from 'components/ui/Loading'
import MultiSelect from 'components/ui/MultiSelect'
+import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
@@ -51,19 +51,48 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
const [uploadedDataUrls, setUploadedDataUrls] = useState([])
const [selectedServices, setSelectedServices] = useState([])
- const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
+ const {
+ data: organizations,
+ isLoading: isLoadingOrganizations,
+ isError: isErrorOrganizations,
+ isSuccess: isSuccessOrganizations,
+ } = useOrganizationsQuery()
// for use in useEffect
const organizationsRef = useLatest(organizations)
- const { data: allProjects, isSuccess: isProjectsSuccess } = useProjectsQuery()
- const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query({
- projectRef: ref,
- })
+ const {
+ data: allProjects,
+ isLoading: isLoadingProjects,
+ isError: isErrorProjects,
+ isSuccess: isSuccessProjects,
+ } = useProjectsQuery()
- const isInitialized = isOrganizationsSuccess && isProjectsSuccess
const projectDefaults: Partial[] = [{ ref: 'no-project', name: 'No specific project' }]
const projects = [...(allProjects ?? []), ...projectDefaults]
+ const selectedProjectFromUrl = projects.find((project) => project.ref === ref)
+ const selectedCategoryFromUrl = CATEGORY_OPTIONS.find((option) => {
+ if (option.value.toLowerCase() === ((category as string) ?? '').toLowerCase()) return option
+ })
+
+ const selectedProjectRef =
+ selectedProjectFromUrl !== undefined
+ ? selectedProjectFromUrl.ref
+ : projects.length > 0
+ ? projects[0].ref
+ : 'no-project'
+ const selectedOrganizationSlug =
+ selectedProjectRef !== 'no-project'
+ ? organizations?.find((org) => {
+ const project = projects.find((project) => project.ref === selectedProjectRef)
+ return org.id === project?.organization_id
+ })?.slug
+ : organizations?.[0]?.slug
+
+ const { data: subscription, isLoading: isLoadingSubscription } = useProjectSubscriptionV2Query(
+ { projectRef: selectedProjectRef },
+ { enabled: selectedProjectRef !== 'no-project' }
+ )
useEffect(() => {
if (!uploadedFiles) return
@@ -78,37 +107,14 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
const { profile } = useProfile()
const respondToEmail = profile?.primary_email ?? 'your email'
- if (!isInitialized) {
- return (
-
-
-
- )
- }
-
- const selectedProject = projects.find((project) => project.ref === ref)
- const selectedCategory = CATEGORY_OPTIONS.find((option) => {
- if (option.value.toLowerCase() === ((category as string) ?? '').toLowerCase()) return option
- })
-
- const initialProjectRef =
- selectedProject !== undefined
- ? selectedProject.ref
- : projects.length > 0
- ? projects[0].ref
- : 'no-project'
- const initialOrganizationSlug =
- initialProjectRef !== 'no-project'
- ? organizations?.find((org) => {
- const project = projects.find((project) => project.ref === initialProjectRef)
- return org.id === project?.organization_id
- })?.slug
- : organizations?.[0]?.slug
const initialValues = {
- category: selectedCategory !== undefined ? selectedCategory.value : CATEGORY_OPTIONS[0].value,
+ category:
+ selectedCategoryFromUrl !== undefined
+ ? selectedCategoryFromUrl.value
+ : CATEGORY_OPTIONS[0].value,
severity: 'Low',
- projectRef: initialProjectRef,
- organizationSlug: initialOrganizationSlug,
+ projectRef: selectedProjectRef,
+ organizationSlug: selectedOrganizationSlug,
library: 'no-library',
subject: subject ?? '',
message: message || '',
@@ -235,6 +241,23 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
}
}, [values.projectRef])
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ if (
+ isSuccessProjects &&
+ isSuccessOrganizations &&
+ allProjects.length > 0 &&
+ organizations.length > 0
+ ) {
+ const updatedValues = {
+ ...values,
+ projectRef: selectedProjectRef,
+ organizationSlug: selectedOrganizationSlug,
+ }
+ resetForm({ values: updatedValues, initialValues: updatedValues })
+ }
+ }, [isSuccessProjects, isSuccessOrganizations])
+
return (
@@ -264,21 +287,40 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
-
- {projects.map((option) => {
- const organization = organizations?.find((x) => x.id === option.organization_id)
- return (
-
- {option.name}
- {organization?.name}
-
- )
- })}
-
+ {isLoadingProjects && (
+
+
Which project is affected?
+
+
+ )}
+ {isErrorProjects && (
+
+
Which project is affected?
+
+
+
Failed to retrieve projects
+
+
+ )}
+ {isSuccessProjects && (
+
+ {projects.map((option) => {
+ const organization = organizations?.find(
+ (x) => x.id === option.organization_id
+ )
+ return (
+
+ {option.name}
+ {organization?.name}
+
+ )
+ })}
+
+ )}
{SEVERITY_OPTIONS.map((option: any) => {
return (
@@ -295,7 +337,8 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
})}
- {subscription ? (
+
+ {values.projectRef !== 'no-project' && subscription && isSuccessProjects ? (
This project is on the{' '}
{subscription.plan.name} plan
@@ -310,25 +353,42 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
)}
- {values.projectRef === 'no-project' && (organizations?.length ?? 0) > 0 && (
+ {isSuccessProjects && values.projectRef === 'no-project' && (
-
- {organizations?.map((option) => {
- return (
-
- {option.name}
-
- )
- })}
-
+ {isLoadingOrganizations && (
+
+
Which organization is affected?
+
+
+ )}
+ {isErrorOrganizations && (
+
+
Which organization is affected?
+
+
+
Failed to retrieve organizations
+
+
+ )}
+ {isSuccessOrganizations && (
+
+ {organizations?.map((option) => {
+ return (
+
+ {option.name}
+
+ )
+ })}
+
+ )}
)}
@@ -369,13 +429,6 @@ const SupportForm = ({ setSentCategory }: SupportFormProps) => {
- {/* {values.category === 'Problem' && (
- <>
-
-
- >
- )} */}
-
{!isDisabled ? (
<>
{['Performance'].includes(values.category) && isFreeProject ? (
diff --git a/studio/components/layouts/DocsLayout/DocsLayout.tsx b/studio/components/layouts/DocsLayout/DocsLayout.tsx
index d14142740f2..10ddcd7c040 100644
--- a/studio/components/layouts/DocsLayout/DocsLayout.tsx
+++ b/studio/components/layouts/DocsLayout/DocsLayout.tsx
@@ -11,7 +11,7 @@ import { generateDocsMenu } from './DocsLayout.utils'
function DocsLayout({ title, children }: { title: string; children: ReactElement }) {
const router = useRouter()
- const { meta } = useStore()
+ const { ui, meta } = useStore()
const { data, isLoading, error } = meta.openApi
const selectedProject = useSelectedProject()
@@ -26,10 +26,10 @@ function DocsLayout({ title, children }: { title: string; children: ReactElement
}
useEffect(() => {
- if (selectedProject?.ref && !isPaused) {
+ if (ui.selectedProjectRef && !isPaused) {
meta.openApi.load()
}
- }, [selectedProject?.ref])
+ }, [ui.selectedProjectRef])
if (error) {
return (
diff --git a/studio/components/layouts/SettingsLayout/SettingsLayout.tsx b/studio/components/layouts/SettingsLayout/SettingsLayout.tsx
index 49e3e05b8ee..fa1edfb8c91 100644
--- a/studio/components/layouts/SettingsLayout/SettingsLayout.tsx
+++ b/studio/components/layouts/SettingsLayout/SettingsLayout.tsx
@@ -34,10 +34,10 @@ const SettingsLayout: FC
= ({ title, children }) => {
)
useEffect(() => {
- if (project?.ref) {
+ if (ui.selectedProjectRef) {
meta.extensions.load()
}
- }, [project?.ref])
+ }, [ui.selectedProjectRef])
return (
{
const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
const freePlanWithExceedingLimits =
- (isSelectFreeTier || orgSubscription?.plan?.id === 'free') && hasMembersExceedingFreeTierLimit
+ ((isSelectFreeTier && !billedViaOrg) || orgSubscription?.plan?.id === 'free') &&
+ hasMembersExceedingFreeTierLimit
const canCreateProject = isAdmin && !freePlanWithExceedingLimits
@@ -559,7 +560,7 @@ const Wizard: NextPageWithLayout = () => {
)}
- {!isSelectFreeTier && (
+ {!billedViaOrg && !isSelectFreeTier && (
<>
{
const canCreateWebhooks = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'triggers')
useEffect(() => {
- if (project?.ref) meta.hooks.load()
- }, [project?.ref])
+ if (ui.selectedProjectRef) meta.hooks.load()
+ }, [ui.selectedProjectRef])
const enableHooksForProject = async () => {
const res = await post(`${API_URL}/database/${ref}/hook-enable`, {})