import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import Link from 'next/link' import { useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, FormInputGroupInput, FormMessage, InputGroup, InputGroupAddon, InputGroupText, Switch, } from 'ui' import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import AlertError from '@/components/ui/AlertError' import { ToggleSpendCapButton } from '@/components/ui/ToggleSpendCapButton' import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton' import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query' import { useMaxConnectionsQuery } from '@/data/database/max-connections-query' import { useRealtimeConfigurationUpdateMutation } from '@/data/realtime/realtime-config-mutation' import { REALTIME_DEFAULT_CONFIG, useRealtimeConfigurationQuery, } from '@/data/realtime/realtime-config-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' const formId = 'realtime-configuration-form' export const RealtimeSettings = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { data: organization, isSuccess: isSuccessOrganization } = useSelectedOrganizationQuery() const { can: canUpdateConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.REALTIME_ADMIN_READ, '*' ) const [isConfirmNextModalOpen, setIsConfirmNextModalOpen] = useState(false) const { data: maxConn } = useMaxConnectionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data, error, isError } = useRealtimeConfigurationQuery({ projectRef, }) const { data: policies, isSuccess: isSuccessPolicies } = useDatabasePoliciesQuery({ projectRef, connectionString: project?.connectionString, schema: 'realtime', }) const isFreePlan = organization?.plan.id === 'free' const isUsageBillingEnabled = organization?.usage_billing_enabled const isRealtimeDisabled = data?.suspend ?? REALTIME_DEFAULT_CONFIG.suspend // Check if RLS policies exist for realtime.messages table const realtimeMessagesPolicies = policies?.filter( (policy) => policy.schema === 'realtime' && policy.table === 'messages' ) const hasRealtimeMessagesPolicies = realtimeMessagesPolicies && realtimeMessagesPolicies.length > 0 const { mutate: updateRealtimeConfig, isPending: isUpdatingConfig } = useRealtimeConfigurationUpdateMutation({ onSuccess: () => { form.reset(form.getValues()) toast.success('Successfully updated realtime settings') setIsConfirmNextModalOpen(false) }, }) const FormSchema = z.discriminatedUnion('suspend', [ z.object({ suspend: z.literal(true), connection_pool: z.coerce .number() .min(1) .max(maxConn?.maxConnections ?? 100) .optional(), max_concurrent_users: z.coerce.number().min(1).max(50000).optional(), max_events_per_second: z.coerce.number().min(1).max(10000).optional(), max_presence_events_per_second: z.coerce.number().min(1).max(10000).optional(), max_payload_size_in_kb: z.coerce.number().min(1).max(3000).optional(), // [Joshen] These fields are temporarily hidden from the UI // max_bytes_per_second: z.coerce.number().min(1).max(10000000).optional(), // max_channels_per_client: z.coerce.number().min(1).max(10000).optional(), // max_joins_per_second: z.coerce.number().min(1).max(5000).optional(), allow_public: z.boolean().optional(), }), z.object({ suspend: z.literal(false), connection_pool: z.coerce .number() .min(1) .max(maxConn?.maxConnections ?? 100), max_concurrent_users: z.coerce.number().min(1).max(50000), max_events_per_second: z.coerce.number().min(1).max(10000), max_presence_events_per_second: z.coerce.number().min(1).max(10000), max_payload_size_in_kb: z.coerce.number().min(1).max(3000), // [Joshen] These fields are temporarily hidden from the UI // max_bytes_per_second: z.coerce.number().min(1).max(10000000), // max_channels_per_client: z.coerce.number().min(1).max(10000), // max_joins_per_second: z.coerce.number().min(1).max(5000), allow_public: z.boolean(), }), ]) const form = useForm>({ resolver: zodResolver(FormSchema as any), defaultValues: { ...REALTIME_DEFAULT_CONFIG, allow_public: !REALTIME_DEFAULT_CONFIG.private_only, }, values: { ...(data ?? REALTIME_DEFAULT_CONFIG), allow_public: !(data?.private_only ?? REALTIME_DEFAULT_CONFIG.private_only), } as any, }) const { allow_public, suspend } = form.watch() const isSettingToPrivate = !data?.private_only && !allow_public const isDisablingRealtime = !isRealtimeDisabled && suspend const isEnablingRealtime = isRealtimeDisabled && !suspend const onSubmit: SubmitHandler> = (_data) => { if (!projectRef) return console.error('Project ref is required') setIsConfirmNextModalOpen(true) } const onConfirmSave = () => { if (!projectRef) return console.error('Project ref is required') const values = form.getValues() // [Joshen] Casting to `Number` here as the values are being set as string when edited in the form // and returned in form.getValues() - I might be missing some easy util function from RHF though updateRealtimeConfig({ ref: projectRef, private_only: !values.allow_public, connection_pool: Number( values.connection_pool ?? data?.connection_pool ?? REALTIME_DEFAULT_CONFIG.connection_pool ), max_concurrent_users: Number( values.max_concurrent_users ?? data?.max_concurrent_users ?? REALTIME_DEFAULT_CONFIG.max_concurrent_users ), max_events_per_second: Number( values.max_events_per_second ?? data?.max_events_per_second ?? REALTIME_DEFAULT_CONFIG.max_events_per_second ), max_presence_events_per_second: Number( values.max_presence_events_per_second ?? data?.max_presence_events_per_second ?? REALTIME_DEFAULT_CONFIG.max_presence_events_per_second ), max_payload_size_in_kb: Number( values.max_payload_size_in_kb ?? data?.max_payload_size_in_kb ?? REALTIME_DEFAULT_CONFIG.max_payload_size_in_kb ), suspend: values.suspend, }) } return ( <>
{isError ? ( ) : ( ( <> field.onChange(!checked)} disabled={!canUpdateConfig} /> )} /> {(isRealtimeDisabled || isDisablingRealtime || isEnablingRealtime) && (
{isDisablingRealtime ? 'Realtime service will be disabled' : isEnablingRealtime ? 'Realtime service will be re-enabled' : isRealtimeDisabled ? 'Realtime service is disabled' : null}

{isDisablingRealtime ? 'Clients will no longer be able to connect to your project’s realtime service once saved' : isEnablingRealtime ? "Clients will be able to connect to your project's realtime service again once saved" : isRealtimeDisabled ? 'You will need to enable it to continue using Realtime' : null}

)}
{!suspend && ( <> ( <> {isSuccessPolicies && !hasRealtimeMessagesPolicies && !allow_public && !isRealtimeDisabled && (

Private mode is {isSettingToPrivate ? 'being ' : ''} enabled, but no RLS policies exists on the{' '} realtime.messages {' '} table. No messages will be received by users.

} /> )} )} />
( <> connections {!!maxConn && field.value && field.value > maxConn.maxConnections * 0.5 && ( )} )} /> ( clients )} /> ( events/s )} /> {isSuccessOrganization && !isUsageBillingEnabled && (
Spend cap needs to be disabled to configure this value

{isFreePlan ? 'Upgrade to the Pro plan first to disable spend cap' : 'You may adjust this setting in the organization billing settings'}

{isFreePlan ? ( ) : ( )}
)}
( events/s )} /> {isSuccessOrganization && !isUsageBillingEnabled && (
Spend cap needs to be disabled to configure this value

{isFreePlan ? 'Upgrade to the Pro plan first to disable spend cap' : 'You may adjust this setting in the organization billing settings'}

{isFreePlan ? ( ) : ( )}
)}
( KB )} /> {isSuccessOrganization && !isUsageBillingEnabled && (
Spend cap needs to be disabled to configure this value

{isFreePlan ? 'Upgrade to the Pro plan first to disable spend cap' : 'You may adjust this setting in the organization billing settings'}

{isFreePlan ? ( ) : ( )}
)}
)}
{isPermissionsLoaded && !canUpdateConfig && (

You need additional permissions to update realtime settings

)}
{form.formState.isDirty && ( )}
)} setIsConfirmNextModalOpen(false)} onConfirm={() => onConfirmSave()} >

Saving the changes will disconnect all the clients connected to your project. Are you sure you want to continue?

) }