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, useWatch } 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/Admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
import { SuspensionNotice } from '../Settings/SuspensionNotice'
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 { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const formId = 'realtime-configuration-form'
const REALTIME_SOFT_LIMITS = {
max_concurrent_users: 300_000,
max_events_per_second: 50_000,
max_presence_events_per_second: 5_000,
max_payload_size_in_kb: 3_000,
}
const MAX_POSTGRES_CHANGES_POOL = 20
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, isPending } = useRealtimeConfigurationQuery({ projectRef })
const { data: policies, isSuccess: isSuccessPolicies } = useDatabasePoliciesQuery({
projectRef,
connectionString: project?.connectionString,
schemas: ['realtime'],
})
// Per-plan realtime ceilings come from the org's entitlements (plan + any overrides).
// The effective limit is the lower of the entitlement and the soft cap, so an unlimited
// plan is still bounded and self-hosted / loading falls back to the soft cap.
const { getEntitlementMax: getMaxConcurrentUsers, isSuccess: isSuccessMaxConcurrentUsers } =
useCheckEntitlements('realtime.max_concurrent_users')
const { getEntitlementMax: getMaxEventsPerSecond, isSuccess: isSuccessMaxEventsPerSecond } =
useCheckEntitlements('realtime.max_events_per_second')
const {
getEntitlementMax: getMaxPresenceEventsPerSecond,
isSuccess: isSuccessMaxPresenceEventsPerSecond,
} = useCheckEntitlements('realtime.max_presence_events_per_second')
const { getEntitlementMax: getMaxPayloadSizeInKb, isSuccess: isSuccessMaxPayloadSizeInKb } =
useCheckEntitlements('realtime.max_payload_size_in_kb')
const isRealtimeEntitlementsLoaded =
isSuccessMaxConcurrentUsers &&
isSuccessMaxEventsPerSecond &&
isSuccessMaxPresenceEventsPerSecond &&
isSuccessMaxPayloadSizeInKb
const isLoading = isPending || !isRealtimeEntitlementsLoaded
const maxConcurrentUsersLimit = Math.min(
getMaxConcurrentUsers() ?? Infinity,
REALTIME_SOFT_LIMITS.max_concurrent_users
)
const maxEventsPerSecondLimit = Math.min(
getMaxEventsPerSecond() ?? Infinity,
REALTIME_SOFT_LIMITS.max_events_per_second
)
const maxPresenceEventsPerSecondLimit = Math.min(
getMaxPresenceEventsPerSecond() ?? Infinity,
REALTIME_SOFT_LIMITS.max_presence_events_per_second
)
const maxPayloadSizeInKbLimit = Math.min(
getMaxPayloadSizeInKb() ?? Infinity,
REALTIME_SOFT_LIMITS.max_payload_size_in_kb
)
const isFreePlan = organization?.plan.id === 'free'
const isUsageBillingEnabled = organization?.usage_billing_enabled
const isRealtimeDisabled = data?.suspend ?? REALTIME_DEFAULT_CONFIG.suspend
const isAdminSuspended = Boolean(data?.admin_suspended_at)
// 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(),
postgres_changes_pool: z.coerce.number().min(1).max(MAX_POSTGRES_CHANGES_POOL).optional(),
max_concurrent_users: z.coerce
.number()
.min(1)
.max(
maxConcurrentUsersLimit,
`Cannot exceed ${maxConcurrentUsersLimit.toLocaleString()} concurrent clients`
)
.optional(),
max_events_per_second: z.coerce
.number()
.min(1)
.max(
maxEventsPerSecondLimit,
`Cannot exceed ${maxEventsPerSecondLimit.toLocaleString()} events per second`
)
.optional(),
max_presence_events_per_second: z.coerce
.number()
.min(1)
.max(
maxPresenceEventsPerSecondLimit,
`Cannot exceed ${maxPresenceEventsPerSecondLimit.toLocaleString()} presence events per second`
)
.optional(),
max_payload_size_in_kb: z.coerce
.number()
.min(1)
.max(
maxPayloadSizeInKbLimit,
`Cannot exceed ${maxPayloadSizeInKbLimit.toLocaleString()} KB`
)
.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),
postgres_changes_pool: z.coerce.number().min(1).max(MAX_POSTGRES_CHANGES_POOL),
max_concurrent_users: z.coerce
.number()
.min(1)
.max(
maxConcurrentUsersLimit,
`Cannot exceed ${maxConcurrentUsersLimit.toLocaleString()} concurrent clients`
),
max_events_per_second: z.coerce
.number()
.min(1)
.max(
maxEventsPerSecondLimit,
`Cannot exceed ${maxEventsPerSecondLimit.toLocaleString()} events per second`
),
max_presence_events_per_second: z.coerce
.number()
.min(1)
.max(
maxPresenceEventsPerSecondLimit,
`Cannot exceed ${maxPresenceEventsPerSecondLimit.toLocaleString()} presence events per second`
),
max_payload_size_in_kb: z.coerce
.number()
.min(1)
.max(
maxPayloadSizeInKbLimit,
`Cannot exceed ${maxPayloadSizeInKbLimit.toLocaleString()} KB`
),
// [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 configValues = data ?? REALTIME_DEFAULT_CONFIG
const sharedFormValues = {
connection_pool: configValues.connection_pool ?? REALTIME_DEFAULT_CONFIG.connection_pool,
postgres_changes_pool:
configValues.postgres_changes_pool ?? REALTIME_DEFAULT_CONFIG.postgres_changes_pool,
max_concurrent_users:
configValues.max_concurrent_users ?? REALTIME_DEFAULT_CONFIG.max_concurrent_users,
max_events_per_second:
configValues.max_events_per_second ?? REALTIME_DEFAULT_CONFIG.max_events_per_second,
max_presence_events_per_second:
configValues.max_presence_events_per_second ??
REALTIME_DEFAULT_CONFIG.max_presence_events_per_second,
max_payload_size_in_kb:
configValues.max_payload_size_in_kb ?? REALTIME_DEFAULT_CONFIG.max_payload_size_in_kb,
allow_public: !(configValues.private_only ?? REALTIME_DEFAULT_CONFIG.private_only),
}
const formValues: z.infer
{isFreePlan ? 'Upgrade to the Pro plan first to disable spend cap' : 'You may adjust this setting in the organization billing settings'}
{isFreePlan ? 'Upgrade to the Pro plan first to disable spend cap' : 'You may adjust this setting in the organization billing settings'}
{isFreePlan ? 'Upgrade to the Pro plan first to disable spend cap' : 'You may adjust this setting in the organization billing settings'}
You need additional permissions to update realtime settings
)}Saving the changes will disconnect all the clients connected to your project. Are you sure you want to continue?