diff --git a/apps/docs/content/guides/auth/social-login/auth-apple.mdx b/apps/docs/content/guides/auth/social-login/auth-apple.mdx index 9cf1b1ddc909d..4c343ce1b54b9 100644 --- a/apps/docs/content/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-apple.mdx @@ -22,6 +22,50 @@ In some cases you're able to use the OAuth flow within web-based native apps suc When developing with Expo, you can test Sign in with Apple via the Expo Go app, in all other cases you will need to obtain an [Apple Developer](https://developer.apple.com) account to enable the capability. + + +If you're using the OAuth flow (web, Flutter web, Kotlin non-iOS platforms), Apple requires you to generate a new secret key every 6 months using the signing key (`.p8` file). This is a critical maintenance task that will cause authentication failures if missed. + +- Set a recurring calendar reminder for every 6 months to rotate your secret key +- Store the `.p8` file securely - you'll need it for each rotation +- If you lose the `.p8` file or it's compromised, immediately revoke it in the Apple Developer Console and create a new one +- Consider automating this process if possible to prevent service disruptions + +This requirement only applies if you're configuring OAuth settings (Services ID, signing key, etc.). Native-only implementations don't require secret key rotation. + + + + + +Apple's identity token does not include the user's full name in its claims. This means the Supabase Auth server cannot automatically populate the user's name metadata when users sign in with Apple. + +- Apple only provides the user's full name during the **first sign-in attempt** (when the user initially authorizes your app) +- All subsequent sign-ins return `null` for the full name fields +- The full name must be captured from Apple's native authentication response and manually saved using the `updateUser` method + +**Recommended Approach:** +After a successful Sign in with Apple, check if the full name is available in the authentication response, and if so, use the `updateUser` method to save it to the user's metadata: + +```typescript +// Example: Handling full name after successful sign in +if (credential.fullName) { + // Full name is only provided on first sign-in + await supabase.auth.updateUser({ + data: { + full_name: `${credential.fullName.givenName} ${credential.fullName.familyName}`, + given_name: credential.fullName.givenName, + family_name: credential.fullName.familyName, + }, + }) +} +``` + +If a user revokes your app's access and then re-authorizes it, Apple will provide the full name again as if it were a first sign-in. + +The platform-specific examples below demonstrate how to implement this pattern for each SDK. + + + + + When using the OAuth flow, the user's full name is not accessible from Apple's response. Apple only provides the full name through native authentication methods (Sign in with Apple JS, or native iOS/macOS SDKs) during the first sign-in. + + If you need to collect user names, consider: + - Using Sign in with Apple JS instead (see below) + - Collecting the name through a separate onboarding form + - Using a profiles table to store user information + + + <$Partial path="oauth_pkce_flow.mdx" /> - ### Configuration [#configuration-web] + ### Configuration [#configuration-web-oauth] You will require the following information: 1. Your Apple Developer account's **Team ID**, which is an alphanumeric string of 10 characters that uniquely identifies the developer of the app. It's often accessible in the upper right-side menu on the Apple Developer Console. - 2. Register email sources for _Sign in with Apple for Email Communication_ which can be found in the [Services](https://developer.apple.com/account/resources/services/list) section of the Apple Developer Console. + 2. Register email sources for _Sign in with Apple for Email Communication_ which can be found in the [Services](https://developer.apple.com/account/resources/services/list) section of the Apple Developer Console. This enables Apple to send relay emails through your domain when users choose to hide their email addresses. 3. An **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple once you create an App ID in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) 4. A **Services ID** which uniquely identifies the web services provided by the app you registered in the previous step. You can create a new Services ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example `com.example.app.web`. 5. Configure Website URLs for the newly created **Services ID**. The web domain you should use is the domain your Supabase project is hosted on. This is usually `.supabase.co` while the redirect URL is `https://.supabase.co/auth/v1/callback`. - 6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever lose access to it, or make it public accidentally, revoke it from the Apple Developer Console and create a new one immediately. You will have to generate a new secret key using this file every 6 months, so make sure you schedule a recurring meeting in your calendar! + 6. Create a signing **Key** in the [Keys](https://developer.apple.com/account/resources/authkeys/list) section of the Apple Developer Console. You can use this key to generate a secret key using the tool below, which is added to your Supabase project's Auth configuration. Make sure you safely store the `AuthKey_XXXXXXXXXX.p8` file. If you ever lose access to it, or make it public accidentally, revoke it from the Apple Developer Console and create a new one immediately. 7. Finally, add the information you configured above to the [Apple provider configuration in the Supabase dashboard](/dashboard/project/_/auth/providers). You can also configure the Apple auth provider using the Management API: @@ -100,33 +155,102 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ You can use the `signInWithIdToken()` method from the Supabase JavaScript library on the website to obtain an access and refresh token once the user has given consent using Sign in with Apple JS: ```ts - function signIn() { - const data = await AppleID.auth.signIn() - - await supabase.auth.signInWithIdToken({ - provider: 'apple', - token: data.id_token, - nonce: '', - }) + async function signIn() { + try { + // Generate a nonce for security + const nonce = crypto.randomUUID() // or use your preferred nonce generation method + + const data = await AppleID.auth.signIn() + + const { data: authData, error } = await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: data.id_token, + nonce: nonce, + }) + + if (error) { + throw error + } + + // Apple only provides the user's name on the first sign-in + // The user object contains name information from Apple's response + if (data.user && data.user.name) { + const fullName = [ + data.user.name.firstName, + data.user.name.middleName, + data.user.name.lastName + ].filter(Boolean).join(' ') + + // Save the name to user metadata for future use + await supabase.auth.updateUser({ + data: { + full_name: fullName, + given_name: data.user.name.firstName, + family_name: data.user.name.lastName, + } + }) + } + } catch (error) { + console.error('Apple sign in failed:', error) + // Handle sign-in errors appropriately + } } ``` Alternatively, you can use the `AppleIDSignInOnSuccess` event with the `usePopup` option: ```ts - // Listen for authorization success. + // Generate and store nonce for verification + const nonce = crypto.randomUUID() + + // Initialize Apple ID with nonce + AppleID.auth.init({ + clientId: 'your-services-id', + scope: 'name email', + redirectURI: 'https://your-domain.com/auth/callback', + usePopup: true, + nonce: nonce, + }) + + // Listen for authorization success document.addEventListener('AppleIDSignInOnSuccess', async (event) => { - await supabase.auth.signInWithIdToken({ - provider: 'apple', - token: event.data.id_token, - nonce: '', - }) + try { + const { data: authData, error } = await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: event.detail.authorization.id_token, + nonce: nonce, + }) + + if (error) { + throw error + } + + // Apple only provides the user's name on the first sign-in + if (event.detail.user && event.detail.user.name) { + const fullName = [ + event.detail.user.name.firstName, + event.detail.user.name.middleName, + event.detail.user.name.lastName + ].filter(Boolean).join(' ') + + // Save the name to user metadata for future use + await supabase.auth.updateUser({ + data: { + full_name: fullName, + given_name: event.detail.user.name.firstName, + family_name: event.detail.user.name.lastName, + } + }) + } + } catch (error) { + console.error('Apple sign in failed:', error) + } }) ``` - Make sure you request for the scope `name email` when initializing the library. + Make sure you request the scope `name email` when initializing the library, as shown in the example above. - ### Configuration [#configuration-apple-js] + ### Configuration [#configuration-web-apple-js] To use Sign in with Apple JS you need to configure these options: @@ -183,6 +307,24 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ }) console.log(JSON.stringify({ error, user }, null, 2)) if (!error) { + // Apple only provides the user's full name on the first sign-in + // Save it to user metadata if available + if (credential.fullName) { + const nameParts = [] + if (credential.fullName.givenName) nameParts.push(credential.fullName.givenName) + if (credential.fullName.middleName) nameParts.push(credential.fullName.middleName) + if (credential.fullName.familyName) nameParts.push(credential.fullName.familyName) + + const fullName = nameParts.join(' ') + + await supabase.auth.updateUser({ + data: { + full_name: fullName, + given_name: credential.fullName.givenName, + family_name: credential.fullName.familyName, + } + }) + } // User is signed in. } } else { @@ -198,13 +340,49 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ }} /> ) - return <>{/* Implement Android Auth options. */} + return ( + <> + {/* + On Android, Sign in with Apple is not natively supported. + You have two options: + 1. Use the OAuth flow via signInWithOAuth (see Flutter Android example below) + 2. Use a web-based solution like react-native-app-auth + + For most cases, we recommend using the OAuth flow: + */} + + + ) } ``` - When working with bare React Native, you can use [invertase/react-native-apple-authentication](https://github.com/invertase/react-native-apple-authentication) to obtain the ID token. + + + - Sign in with Apple is not natively available on Android devices + - The OAuth flow opens a browser window for authentication + - You must configure [deep linking](/docs/guides/auth/native-mobile-deep-linking) for the callback to work properly + - The OAuth configuration (Services ID, etc.) must be set up as described in the [Web OAuth Configuration section](#configuration-web-oauth) + + + + When working with bare React Native, you can use [invertase/react-native-apple-authentication](https://github.com/invertase/react-native-apple-authentication) to obtain the ID token on iOS. For Android, use the OAuth flow as shown above. - ### Configuration [#expo-configuration-native-app] + ### Configuration [#configuration-expo-native] @@ -231,10 +409,10 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ - ## Apple sign in on iOS and macOS + ## Sign in with Apple on iOS and macOS - You can perform Apple sign in using the [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) package on Flutter apps running on iOS or macOS. - Follow the instructions in the package README to set up native Apple sign in on iOS and macOS. + You can perform Sign in with Apple using the [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) package on Flutter apps running on iOS or macOS. + Follow the instructions in the package README to set up native Sign in with Apple on iOS and macOS. Once the setup is complete on the Flutter app, add the bundle ID of your app to your Supabase dashboard in `Authentication -> Providers -> Apple` in order to register your app with Supabase. @@ -262,22 +440,44 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ 'Could not find ID Token from generated credential.'); } - return supabase.auth.signInWithIdToken( + final authResponse = await supabase.auth.signInWithIdToken( provider: OAuthProvider.apple, idToken: idToken, nonce: rawNonce, ); + + // Apple only provides the user's full name on the first sign-in + // Save it to user metadata if available + if (credential.givenName != null || credential.familyName != null) { + final nameParts = []; + if (credential.givenName != null) nameParts.add(credential.givenName!); + if (credential.familyName != null) nameParts.add(credential.familyName!); + + final fullName = nameParts.join(' '); + + await supabase.auth.updateUser( + UserAttributes( + data: { + 'full_name': fullName, + 'given_name': credential.givenName, + 'family_name': credential.familyName, + }, + ), + ); + } + + return authResponse; } ``` - ### Configuration [#flutter-configuration-native-app] + ### Configuration [#configuration-flutter-native] 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](/dashboard/project/_/auth/providers) under _Client IDs_. - ## Apple sign in on Android, Web, Windows and Linux + ## Sign in with Apple on Android, Web, Windows and Linux - For platforms that doesn't support native Apple sign in, you can use the `signInWithOAuth()` method to perform the Apple sign in. + For platforms that don't support native Sign in with Apple, you can use the `signInWithOAuth()` method to perform Sign in with Apple. @@ -298,7 +498,7 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ This call takes the user to Apple's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Supabase Auth before it redirects back to your Flutter application with an access and refresh token representing the user's session. - ### Configuration [#flutter-configuration-web] + ### Configuration [#configuration-flutter-web] You will require the following information: @@ -352,14 +552,47 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ else { return } - try await client.auth.signInWithIdToken( + + try await client.auth.signInWithIdToken( credentials: .init( provider: .apple, idToken: idToken ) ) + + // Apple only provides the user's full name on the first sign-in + // Save it to user metadata if available + if let fullName = credential.fullName { + var nameParts: [String] = [] + if let givenName = fullName.givenName { + nameParts.append(givenName) + } + if let middleName = fullName.middleName { + nameParts.append(middleName) + } + if let familyName = fullName.familyName { + nameParts.append(familyName) + } + + let fullNameString = nameParts.joined(separator: " ") + + try await client.auth.update( + user: UserAttributes( + data: [ + "full_name": .string(fullNameString), + "given_name": .string(fullName.givenName ?? ""), + "family_name": .string(fullName.familyName ?? "") + ] + ) + ) + } + + // User successfully signed in + print("Sign in with Apple successful!") } catch { - dump(error) + // Handle sign-in errors + print("Sign in with Apple failed: \(error.localizedDescription)") + // Show error alert to user } } } @@ -368,7 +601,7 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ } ``` - ### Configuration [#swift-configuration-native-app] + ### Configuration [#configuration-swift-native] 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](/dashboard/project/_/auth/providers) under _Client IDs_. @@ -408,12 +641,55 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ ```kotlin val authState = supabaseClient.composeAuth.rememberLoginWithApple( - onResult = { - when(it) { //handle errors - NativeSignInResult.ClosedByUser -> TODO() - is NativeSignInResult.Error -> TODO() - is NativeSignInResult.NetworkError -> TODO() - NativeSignInResult.Success -> TODO() + onResult = { result -> + when(result) { + NativeSignInResult.ClosedByUser -> { + // User cancelled the sign-in flow + println("User cancelled Apple sign in") + } + is NativeSignInResult.Error -> { + // An error occurred during sign in + println("Apple sign in error: ${result.message}") + // Show error to user + } + is NativeSignInResult.NetworkError -> { + // Network error occurred + println("Network error during Apple sign in: ${result.error}") + // Show network error to user + } + is NativeSignInResult.Success -> { + // User successfully signed in + println("Apple sign in successful!") + + // Apple only provides the user's full name on the first sign-in (iOS only) + // Save it to user metadata if available + result.data?.let { appleData -> + appleData.fullName?.let { fullName -> + val nameParts = mutableListOf() + fullName.givenName?.let { nameParts.add(it) } + fullName.middleName?.let { nameParts.add(it) } + fullName.familyName?.let { nameParts.add(it) } + + val fullNameString = nameParts.joinToString(" ") + + scope.launch { + try { + supabaseClient.auth.updateUser { + data = buildJsonObject { + put("full_name", fullNameString) + fullName.givenName?.let { put("given_name", it) } + fullName.familyName?.let { put("family_name", it) } + } + } + } catch (e: Exception) { + println("Failed to update user metadata: ${e.message}") + } + } + } + } + + // Navigate to home screen or update UI + } } } ) @@ -423,6 +699,44 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ } ``` + ### Configuration [#configuration-kotlin] + + **For iOS (native Sign in with Apple):** + + 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. + 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](/dashboard/project/_/auth/providers) under _Client IDs_. + + **For other platforms (Android, Desktop, Web):** + + On non-iOS platforms, ComposeAuth automatically falls back to the OAuth flow. You need to configure the OAuth settings as described in the [Web OAuth Configuration section](#configuration-web-oauth) above, including: + + - Team ID + - Email sources registration + - Services ID + - Signing Key and secret generation + + **Dependencies:** + + Add the following to your `build.gradle.kts`: + + ```kotlin + dependencies { + implementation("io.github.jan-tennert.supabase:gotrue-kt:VERSION") + implementation("io.github.jan-tennert.supabase:compose-auth:VERSION") + implementation("io.github.jan-tennert.supabase:compose-auth-ui:VERSION") // Optional, for UI components + } + ``` + + + + **Platform-Specific Notes** + + - **iOS**: Uses native Apple Authentication Services automatically + - **Android/Desktop/Web**: Falls back to OAuth flow (requires web OAuth configuration) + - **Minimum Versions**: Kotlin 1.9.0+, Compose Multiplatform 1.5.0+ + + + diff --git a/apps/studio/README.md b/apps/studio/README.md index 8ca189e0fa76e..7f8fd0a63bb33 100644 --- a/apps/studio/README.md +++ b/apps/studio/README.md @@ -40,11 +40,11 @@ Project settings are managed outside of the Dashboard. If you use docker compose # You'll need to be on Node v20 # in /studio -npm i # install dependencies -npm run dev:secrets:pull # Supabase internal use: if you are working on the platform version of the Studio -npm run dev # start dev server -npm run test # run tests -npm run -- --watch # run tests in watch mode +pnpmn install # install dependencies +mise studio # Supabase internal use: if you are working on the platform version of the Studio +pnpm run dev # start dev server +pnpm run test # run tests +pnpm run test -- --watch # run tests in watch mode ``` ## Running within a self-hosted environment diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index 9d1b038e27897..83e752106725a 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -1,9 +1,10 @@ -import { ControllerRenderProps, UseFormReturn } from 'react-hook-form' +import { ControllerRenderProps } from 'react-hook-form' import { useFlag, useParams } from 'common' import AlertError from 'components/ui/AlertError' 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 type { CloudProvider } from 'shared-data' import { @@ -27,7 +28,7 @@ import { getAvailableRegions } from './ProjectCreation.utils' interface RegionSelectorProps { cloudProvider: CloudProvider field: ControllerRenderProps - form: UseFormReturn + instanceSize?: DesiredInstanceSize layout?: 'vertical' | 'horizontal' } @@ -38,6 +39,7 @@ interface RegionSelectorProps { export const RegionSelector = ({ cloudProvider, field, + instanceSize, layout = 'horizontal', }: RegionSelectorProps) => { const { slug } = useParams() @@ -54,7 +56,10 @@ export const RegionSelector = ({ isLoading: isLoadingAvailableRegions, isError: isErrorAvailableRegions, error: errorAvailableRegions, - } = useOrganizationAvailableRegionsQuery({ slug, cloudProvider }, { enabled: smartRegionEnabled }) + } = useOrganizationAvailableRegionsQuery( + { slug, cloudProvider, desiredInstanceSize: instanceSize }, + { enabled: smartRegionEnabled, staleTime: 1000 * 60 * 5 } // 5 minutes + ) const smartRegions = availableRegionsData?.all.smartGroup ?? [] const allRegions = availableRegionsData?.all.specific ?? [] diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx index 1e5ac85c02584..e001132c53529 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx @@ -1,6 +1,6 @@ import { PlusCircle } from 'lucide-react' import Link from 'next/link' -import { Dispatch, SetStateAction, useState } from 'react' +import { Dispatch, SetStateAction, useState, useEffect } from 'react' import { useParams } from 'common' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' @@ -38,6 +38,11 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter const { data: org } = useSelectedOrganizationQuery() const { mutate: sendEvent } = useSendEventMutation() + // Update tempConfig when config changes to ensure consistency + useEffect(() => { + setTempConfig(config) + }, [config]) + const onOpen = (v: boolean) => { // when opening, copy the outside config into the intermediate one if (v === true) { @@ -127,9 +132,15 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter
-
@@ -137,17 +148,20 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter id="toggle-db-changes" size="tiny" checked={tempConfig.enableDbChanges} + disabled={!config.enableDbChanges} onChange={() => setTempConfig({ ...tempConfig, enableDbChanges: !tempConfig.enableDbChanges }) } />

- Listen for Database inserts, updates, deletes and more + {config.enableDbChanges + ? 'Listen for Database inserts, updates, deletes and more' + : 'Enable realtime publications to listen for database changes'}

- {tempConfig.enableDbChanges && ( + {tempConfig.enableDbChanges && config.enableDbChanges && ( <>
Filter messages from database changes diff --git a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx index 116ce210093a3..098da67887cb7 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx @@ -1,10 +1,10 @@ import { useParams } from 'common' import { useState, useEffect } from 'react' -import { motion } from 'framer-motion' -import { MousePointer2 } from 'lucide-react' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Header } from './Header' import MessagesTable from './MessagesTable' import { SendMessageModal } from './SendMessageModal' @@ -17,6 +17,19 @@ import { EmptyRealtime } from './EmptyRealtime' export const RealtimeInspector = () => { const { ref } = useParams() const { data: org } = useSelectedOrganizationQuery() + const { data: project } = useSelectedProjectQuery() + + // Check if realtime publications are available + const { data: publications } = useDatabasePublicationsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const realtimePublication = (publications ?? []).find( + (publication) => publication.name === 'supabase_realtime' + ) + const isRealtimeAvailable = + !!realtimePublication && + ((realtimePublication?.tables ?? []).length > 0 || realtimePublication?.tables === null) const [sendMessageShown, setSendMessageShown] = useState(false) const [realtimeConfig, setRealtimeConfig] = useState({ @@ -31,13 +44,18 @@ export const RealtimeInspector = () => { filter: undefined, bearer: null, enablePresence: true, - enableDbChanges: true, + enableDbChanges: isRealtimeAvailable, // Initialize based on publications availability enableBroadcast: true, }) const { mutate: sendEvent } = useSendEventMutation() const { logData, sendMessage } = useRealtimeMessages(realtimeConfig, setRealtimeConfig) + // Update enableDbChanges when publications change + useEffect(() => { + setRealtimeConfig((prev) => ({ ...prev, enableDbChanges: isRealtimeAvailable })) + }, [isRealtimeAvailable]) + return (
diff --git a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts index adfff8d9a1e9b..40643867f1b20 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts +++ b/apps/studio/components/interfaces/Realtime/Inspector/useRealtimeMessages.ts @@ -168,7 +168,7 @@ export const useRealtimeMessages = ( } // Finally, subscribe to the Channel we just setup - newChannel.subscribe(async (status) => { + newChannel.subscribe(async (status, err) => { if (status === 'SUBSCRIBED') { // Let LiveView know we connected so we can update the button text // pushMessageTo('#conn_info', 'broadcast_subscribed', { host: host }) @@ -192,9 +192,11 @@ export const useRealtimeMessages = ( }) } } else if (status === 'CHANNEL_ERROR') { - toast.error( - `Failed to connect to the channel ${channelName}: This may be due to restrictive RLS policies. Check your role and try again.` - ) + if (err?.message) { + toast.error(`Failed to connect with the following error: ${err.message}`) + } else { + toast.error(`Failed to connect. Please check your RLS policies and try again.`) + } newChannel.unsubscribe() setChannel(undefined) diff --git a/apps/studio/data/organizations/keys.ts b/apps/studio/data/organizations/keys.ts index a20248ab4052b..64dabc7a7f67a 100644 --- a/apps/studio/data/organizations/keys.ts +++ b/apps/studio/data/organizations/keys.ts @@ -1,3 +1,5 @@ +import type { DesiredInstanceSizeForAvailableRegions } from './organization-available-regions-query' + export const organizationKeys = { list: () => ['organizations'] as const, detail: (slug?: string) => ['organizations', slug] as const, @@ -20,6 +22,9 @@ export const organizationKeys = { ['organizations', slug, 'validate-token', token] as const, projectClaim: (slug: string, token: string) => ['organizations', slug, 'project-claim', token] as const, - availableRegions: (slug: string | undefined, cloudProvider: string) => - ['organizations', slug, 'available-regions', cloudProvider] as const, + availableRegions: ( + slug: string | undefined, + cloudProvider: string, + size?: DesiredInstanceSizeForAvailableRegions + ) => ['organizations', slug, 'available-regions', cloudProvider, size] as const, } diff --git a/apps/studio/data/organizations/organization-available-regions-query.ts b/apps/studio/data/organizations/organization-available-regions-query.ts index 01bcd9af28f14..7d037845ae965 100644 --- a/apps/studio/data/organizations/organization-available-regions-query.ts +++ b/apps/studio/data/organizations/organization-available-regions-query.ts @@ -1,16 +1,21 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import type { operations } from 'api-types' import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { organizationKeys } from './keys' +export type DesiredInstanceSizeForAvailableRegions = + operations['v1-get-available-regions']['parameters']['query']['desired_instance_size'] + export type OrganizationAvailableRegionsVariables = { slug?: string cloudProvider: 'AWS' | 'FLY' | 'AWS_K8S' | 'AWS_NIMBUS' + desiredInstanceSize?: DesiredInstanceSizeForAvailableRegions } export async function getOrganizationAvailableRegions( - { slug, cloudProvider }: OrganizationAvailableRegionsVariables, + { slug, cloudProvider, desiredInstanceSize }: OrganizationAvailableRegionsVariables, signal?: AbortSignal ) { if (!slug) throw new Error('slug is required') @@ -20,6 +25,7 @@ export async function getOrganizationAvailableRegions( query: { cloud_provider: cloudProvider, organization_slug: slug, + desired_instance_size: desiredInstanceSize, }, }, signal, @@ -34,7 +40,7 @@ export type OrganizationAvailableRegionsData = Awaited< export type OrganizationAvailableRegionsError = ResponseError export const useOrganizationAvailableRegionsQuery = ( - { slug, cloudProvider }: OrganizationAvailableRegionsVariables, + { slug, cloudProvider, desiredInstanceSize }: OrganizationAvailableRegionsVariables, { enabled = true, ...options @@ -44,8 +50,10 @@ export const useOrganizationAvailableRegionsQuery = = {} ) => - useQuery( - organizationKeys.availableRegions(slug, cloudProvider), - ({ signal }) => getOrganizationAvailableRegions({ slug, cloudProvider }, signal), - { enabled: enabled && typeof slug !== 'undefined', ...options } - ) + useQuery({ + queryKey: organizationKeys.availableRegions(slug, cloudProvider, desiredInstanceSize), + queryFn: ({ signal }) => + getOrganizationAvailableRegions({ slug, cloudProvider, desiredInstanceSize }, signal), + enabled: enabled && typeof slug !== 'undefined', + ...options, + }) diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 76f66587a5868..9d4dbad820c3d 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -1,7 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { debounce } from 'lodash' -import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react' @@ -121,7 +120,7 @@ const FormSchema = z.object({ dbPass: z .string({ required_error: 'Please enter a database password.' }) .min(1, 'Password is required.'), - instanceSize: z.string(), + instanceSize: z.string().optional(), dataApi: z.boolean(), useApiSchema: z.boolean(), postgresVersionSelection: z.string(), @@ -133,17 +132,29 @@ export type CreateProjectForm = z.infer const Wizard: NextPageWithLayout = () => { const router = useRouter() const { slug, projectName } = useParams() + const defaultProvider = useDefaultProvider() + const { data: currentOrg } = useSelectedOrganizationQuery() const isFreePlan = currentOrg?.plan?.id === 'free' + const canChooseInstanceSize = !isFreePlan + const [lastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, '' ) + const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects') - const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config') + const smartRegionEnabled = useFlag('enableSmartRegion') + const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate') + const showPostgresVersionSelector = useFlag('showPostgresVersionSelector') + const cloudProviderEnabled = useFlag('enableFlyCloudProvider') + const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config') const { infraCloudProviders: validCloudProviders } = useCustomContent(['infra:cloud_providers']) + const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' + const isManagedByVercel = currentOrg?.managed_by === 'vercel-marketplace' + // This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org if (slug === 'last-visited-org') { if (lastVisitedOrganization) { @@ -153,33 +164,69 @@ const Wizard: NextPageWithLayout = () => { } } + const [allProjects, setAllProjects] = useState(undefined) + const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') + const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('') + const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] = + useState(false) + const { mutate: sendEvent } = useSendEventMutation() - const smartRegionEnabled = useFlag('enableSmartRegion') - const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate') - const showPostgresVersionSelector = useFlag('showPostgresVersionSelector') - const cloudProviderEnabled = useFlag('enableFlyCloudProvider') + FormSchema.superRefine(({ dbPassStrength }, refinementContext) => { + if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) { + refinementContext.addIssue({ + code: z.ZodIssueCode.custom, + path: ['dbPass'], + message: passwordStrengthWarning || 'Password not secure enough', + }) + } + }) + + const form = useForm>({ + resolver: zodResolver(FormSchema), + mode: 'onChange', + defaultValues: { + organization: slug, + projectName: projectName || '', + postgresVersion: '', + cloudProvider: PROVIDERS[defaultProvider].id, + dbPass: '', + dbPassStrength: 0, + dbRegion: undefined, + instanceSize: canChooseInstanceSize ? sizes[0] : undefined, + dataApi: true, + useApiSchema: false, + postgresVersionSelection: '', + useOrioleDb: false, + }, + }) + const { instanceSize: watchedInstanceSize, cloudProvider, dbRegion, organization } = form.watch() + + // [Charis] Since the form is updated in a useEffect, there is an edge case + // when switching from free to paid, where canChooseInstanceSize is true for + // an in-between render, but watchedInstanceSize is still undefined from the + // form state carried over from the free plan. To avoid this, we set a + // default instance size in this case. + const instanceSize = canChooseInstanceSize ? watchedInstanceSize ?? sizes[0] : undefined const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery( { slug }, { enabled: isFreePlan } ) + const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 + const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit + + const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery() + const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined + const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug + const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess const { data: approvedOAuthApps } = useAuthorizedAppsQuery( { slug }, { enabled: !isFreePlan && slug !== '_' } ) - const hasOAuthApps = approvedOAuthApps && approvedOAuthApps.length > 0 - const [passwordStrengthMessage, setPasswordStrengthMessage] = useState('') - const [passwordStrengthWarning, setPasswordStrengthWarning] = useState('') - - const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] = - useState(false) - - const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery() - const isNotOnTeamOrEnterprisePlan = useMemo( () => !['team', 'enterprise'].includes(currentOrg?.plan.id ?? ''), [currentOrg] @@ -188,7 +235,6 @@ const Wizard: NextPageWithLayout = () => { const { data: allOverdueInvoices } = useOverdueInvoicesQuery({ enabled: isNotOnTeamOrEnterprisePlan, }) - const overdueInvoices = (allOverdueInvoices ?? []).filter( (x) => x.organization_id === currentOrg?.id ) @@ -219,14 +265,9 @@ const Wizard: NextPageWithLayout = () => { () => orgProjectsFromApi?.pages.flatMap((page) => page.projects), [orgProjectsFromApi?.pages] ) - - const [allProjects, setAllProjects] = useState(undefined) - const organizationProjects = allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? [] - const defaultProvider = useDefaultProvider() - const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery( { cloudProvider: PROVIDERS[defaultProvider].id, @@ -246,6 +287,7 @@ const Wizard: NextPageWithLayout = () => { { slug: slug, cloudProvider: PROVIDERS[defaultProvider].id, + desiredInstanceSize: instanceSize as DesiredInstanceSize, }, { enabled: smartRegionEnabled, @@ -255,7 +297,6 @@ const Wizard: NextPageWithLayout = () => { refetchOnReconnect: false, } ) - const regionError = smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS' ? availableRegionsError @@ -267,68 +308,9 @@ const Wizard: NextPageWithLayout = () => { ? availableRegionsData?.recommendations.smartGroup.name : _defaultRegion - const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects') - - const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined - const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug - const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess - - const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 - - const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' - - const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit - - const isManagedByVercel = currentOrg?.managed_by === 'vercel-marketplace' - const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !isManagedByVercel && !hasOutstandingInvoices - const delayedCheckPasswordStrength = useRef( - debounce((value) => checkPasswordStrength(value), 300) - ).current - - async function checkPasswordStrength(value: any) { - const { message, warning, strength } = await passwordStrength(value) - - form.setValue('dbPassStrength', strength) - form.trigger('dbPassStrength') - form.trigger('dbPass') - - setPasswordStrengthWarning(warning) - setPasswordStrengthMessage(message) - } - - FormSchema.superRefine(({ dbPassStrength }, refinementContext) => { - if (dbPassStrength < DEFAULT_MINIMUM_PASSWORD_STRENGTH) { - refinementContext.addIssue({ - code: z.ZodIssueCode.custom, - path: ['dbPass'], - message: passwordStrengthWarning || 'Password not secure enough', - }) - } - }) - - const form = useForm>({ - resolver: zodResolver(FormSchema), - mode: 'onChange', - defaultValues: { - organization: slug, - projectName: projectName || '', - postgresVersion: '', - cloudProvider: PROVIDERS[defaultProvider].id, - dbPass: '', - dbPassStrength: 0, - dbRegion: defaultRegion || undefined, - instanceSize: sizes[0], - dataApi: true, - useApiSchema: false, - postgresVersionSelection: '', - useOrioleDb: false, - }, - }) - - const { instanceSize, cloudProvider, dbRegion, organization } = form.watch() const dbRegionExact = smartRegionToExactRegion(dbRegion) const availableOrioleVersion = useAvailableOrioleImageVersion( @@ -359,6 +341,20 @@ const Wizard: NextPageWithLayout = () => { ? 0 : instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits + async function checkPasswordStrength(value: any) { + const { message, warning, strength } = await passwordStrength(value) + + form.setValue('dbPassStrength', strength) + form.trigger('dbPassStrength') + form.trigger('dbPass') + + setPasswordStrengthWarning(warning) + setPasswordStrengthMessage(message) + } + const delayedCheckPasswordStrength = useRef( + debounce((value) => checkPasswordStrength(value), 300) + ).current + // [Refactor] DB Password could be a common component used in multiple pages with repeated logic function generatePassword() { const password = generateStrongPassword() @@ -479,6 +475,16 @@ const Wizard: NextPageWithLayout = () => { } }, [regionError]) + useEffect(() => { + if (watchedInstanceSize !== instanceSize) { + form.setValue('instanceSize', instanceSize, { + shouldDirty: false, + shouldValidate: false, + shouldTouch: false, + }) + } + }, [instanceSize, watchedInstanceSize, form]) + return (
@@ -747,7 +753,7 @@ const Wizard: NextPageWithLayout = () => { )} - {currentOrg?.plan && currentOrg?.plan.id !== 'free' && ( + {canChooseInstanceSize && ( { render={({ field }) => ( )} /> diff --git a/apps/www/_customers/rally.mdx b/apps/www/_customers/rally.mdx new file mode 100644 index 0000000000000..5851e5fa1d670 --- /dev/null +++ b/apps/www/_customers/rally.mdx @@ -0,0 +1,138 @@ +--- +name: Rally +title: Rally builds a pan-European fleet payments platform on Supabase +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: +description: Rally went from first line of code to fully licensed fintech in three months, processing live fleet payments across Europe. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: Discover how Rally went from first line of code to fully licensed fintech in three months, processing live fleet payments across Europe. +author: prashant +author_title: Prashant Sridharan +author_url: https://github.com/CoolAssPuppy +author_image_url: https://avatars.githubusercontent.com/u/914007?v=4 +logo: /images/customers/logos/rally.png +logo_inverse: /images/customers/logos/light/rally.png +og_image: /images/customers/og/rally.jpg +tags: + - supabase +date: '2025-10-21' +company_url: https://www.getrally.com +stats: [{ stat: '3 months', label: Time to market }, { stat: '1 week', label: SOC 2 compliance }] +misc: [{ label: 'Founded', text: 'Europe' }] +about: Rally is building a financial platform for fleets across Europe, starting with a modern fuel card that lets businesses pay for fuel, tolls, parking, and EV charging. +# "healthcare" | "fintech" | "ecommerce" | "education" | "gaming" | "media" | "real-estate" | "saas" | "social" | "analytics" | "ai" | "developer-tools" +industry: ['fintech'] +# "startup" | "enterprise" | "indie_dev" +company_size: 'startup' +# "Asia" | "Europe" | "North America" | "South America" | "Africa" | "Oceania" +region: 'Europe' +# "database" | "auth" | "storage" | "realtime" | "functions" | "vector" +supabase_products: ['database', 'auth', 'storage', 'realtime'] +--- + + + We could not have built this company without Supabase. If I had to go and build all these + components myself, we wouldn't even have launched. + + +## Introduction + +[Rally](https://www.getrally.com) is building a financial platform for fleets across Europe, starting with a modern fuel card that lets businesses pay for everything from fuel to tolls, parking, and EV charging. + +Founded by former Stripe, Meta, and Booking product manager **[Thiago Peres](https://www.linkedin.com/in/thiagoperespm/)**, Rally serves medium to large European businesses with vehicles and drivers on the road, from delivery and service companies to quick-service restaurant chains. + +What began as a solo project quickly became a fully licensed fintech company processing live transactions across Europe, all built on Supabase. + +## The challenge + +[Rally](https://www.getrally.com) set out to modernize fleet payments by simplifying expense management, reducing fraud, and giving fleet managers real-time visibility into spend. + +To achieve that, they needed a backend that was robust, secure, and compliant, but also fast to build with. As a first-time builder returning to code after a decade in product management, Peres wanted to focus on the product experience rather than setting up infrastructure. + + + We're dealing with financial information. You can't afford data inconsistencies or downtime. We + needed something resilient from day one. + + +Building a fintech platform typically means orchestrating databases, file storage, auth systems, and messaging queues under strict compliance requirements. For a small team, that complexity could have made Rally's launch impossible. + +## Choosing Supabase + +When Peres began experimenting with prototypes, Supabase stood out immediately. + + + It was very developer centric. The documentation was so good that it felt like part of the + product. Everything worked together — database, storage, functions — in a cohesive way. + + +Supabase offered the power and reliability of Postgres combined with real-time APIs, Auth, Storage, and Edge Functions integrated out of the box. For a founder-CTO building solo, that cohesion made Rally possible. + +Peres also valued Supabase's open-source model, which ensured flexibility and long-term control. + + + Having seen how lock-in works elsewhere, open source gave me peace of mind. If Supabase works, + amazing, but if it didn't, I wouldn't be stuck. + + +Compared to alternatives like Firebase or traditional cloud databases, Supabase offered the right balance of scalability, transparency, and developer experience with a predictable cost structure that is critical for a fast-moving fintech startup. + +## The approach + +Rally's MVP moved from concept to production in just three months. Built entirely by Peres, the company's first banking-backed card went live to paying customers by January, just weeks after he began writing TypeScript and experimenting with Supabase. + +Supabase now powers Rally's entire backend: + +- **[Database](/database) and resilience** – Postgres serves as the source of truth for every transaction, driver, and policy rule. Features like point-in-time recovery and managed load balancing give Rally confidence in handling critical financial data. +- **[Storage](/storage)** – Rally processes thousands of invoices and receipts, leveraging Supabase Storage and image transformations to manage and display documents securely. +- **[Realtime](/realtime)** – Used in Rally's in-house messaging system, especially its WhatsApp-based DriverLink, which lets drivers submit receipts and mileage instantly. +- **[Auth](/auth)** – Manages user access across roles, from fleet managers to finance teams. + +A key Rally innovation built on Supabase is Automatch, an AI-driven system that matches any WhatsApp-submitted receipt to the right transaction automatically. + + + Supabase made it easy to build Automatch. Storage handles receipts securely, and Postgres keeps + every workflow in sync, which is critical when you're dealing with money. + + +## The results + +In less than a year, Rally scaled from prototype to production fintech serving fleets across Europe without a dedicated backend team. Supabase helped Rally move fast, stay compliant, and deliver reliability to customers who depend on uninterrupted access to funds. + +- Three months to market from first line of code to live card transactions +- Zero downtime during incidents, even during upstream AWS outages +- SOC 2 compliance in one week, supported by Supabase's infrastructure and security controls + + + One of the reasons we got our SOC 2 very, very quickly is because we were using Supabase. When all + the requirements came on top of us, and also all the laws and regulations that we had to comply + with, it was actually not very hard. We got our SOC 2 in a week. We did the work upfront to put + the safeguards in place. + + +Rally continues to expand, adding EV charging payments and analytics dashboards powered by Supabase. The next phase includes sustainability benchmarking and AI-driven insights for fleet managers. + + + Every time I think of a feature I'd love Supabase to have, it shows up a few months later. It's + become my database provider, period. + + +## Why Supabase? + +Rally's journey reflects the reasons developers and fast-moving teams choose Supabase: + +- **Faster time to market** – Supabase eliminates backend friction, enabling teams to build and ship faster +- **Integrated suite of tools** – Developers get a complete platform including Postgres, Auth, Storage, Realtime, and more +- **Developer-centric experience** – Comprehensive documentation and cohesive APIs make building intuitive +- **Built for compliance** – Infrastructure and security controls help fintech companies meet regulatory requirements +- **Open-source foundation** – Flexibility and control without vendor lock-in + + + Don't try to reinvent the wheel, especially on fintech. Security and compliance is really at the + core of every fintech. If you go with a provider like Supabase, you can leverage a lot of things + like the backups and distribution and infrastructure. That can give you a lot of peace of mind + when something goes wrong. + + +## **Ready to build and scale with Supabase?** + +Start your journey today at [www.supabase.com](/) diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index 4432958691517..395ff6b4a9317 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Sun, 17 Aug 2025 00:00:00 -0700 + Tue, 21 Oct 2025 00:00:00 -0700 + https://supabase.com/customers/rally + Rally builds a pan-European fleet payments platform on Supabase + https://supabase.com/customers/rally + Rally went from first line of code to fully licensed fintech in three months, processing live fleet payments across Europe. + Tue, 21 Oct 2025 00:00:00 -0700 + + https://supabase.com/customers/soshi From Hackathon to Funded Startup: How Soshi Built an AI Social Media Manager on Supabase https://supabase.com/customers/soshi diff --git a/apps/www/public/images/blog/avatars/thiago-peres-rally.jpeg b/apps/www/public/images/blog/avatars/thiago-peres-rally.jpeg new file mode 100644 index 0000000000000..47add5a9e6d1f Binary files /dev/null and b/apps/www/public/images/blog/avatars/thiago-peres-rally.jpeg differ diff --git a/apps/www/public/images/customers/logos/light/rally.png b/apps/www/public/images/customers/logos/light/rally.png new file mode 100644 index 0000000000000..8c28334851fab Binary files /dev/null and b/apps/www/public/images/customers/logos/light/rally.png differ diff --git a/apps/www/public/images/customers/logos/rally.png b/apps/www/public/images/customers/logos/rally.png new file mode 100644 index 0000000000000..e4203e3dc3104 Binary files /dev/null and b/apps/www/public/images/customers/logos/rally.png differ diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index e43f935e651da..2f259388f73e2 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -10809,6 +10809,28 @@ export interface operations { query: { /** @description Continent code to determine regional recommendations: NA (North America), SA (South America), EU (Europe), AF (Africa), AS (Asia), OC (Oceania), AN (Antarctica) */ continent?: 'NA' | 'SA' | 'EU' | 'AF' | 'AS' | 'OC' | 'AN' + /** @description Desired instance size */ + desired_instance_size?: + | 'pico' + | 'nano' + | 'micro' + | 'small' + | 'medium' + | 'large' + | 'xlarge' + | '2xlarge' + | '4xlarge' + | '8xlarge' + | '12xlarge' + | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' /** @description Slug of your organization */ organization_slug: string } diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 15dde790e7c2c..335e57b6bb1d7 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -24236,6 +24236,27 @@ export interface operations { parameters: { query: { cloud_provider: 'AWS' | 'FLY' | 'AWS_K8S' | 'AWS_NIMBUS' + desired_instance_size?: + | 'pico' + | 'nano' + | 'micro' + | 'small' + | 'medium' + | 'large' + | 'xlarge' + | '2xlarge' + | '4xlarge' + | '8xlarge' + | '12xlarge' + | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' organization_slug: string } header?: never diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index 6ab6fb2ec72b7..0d1db5f69a20f 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -129,7 +129,7 @@ async function debuggableNavigatorLock( let stackException: any try { - throw new Error('Lock is being held for over 2s here') + throw new Error('Lock is being held for over 10s here') } catch (e: any) { stackException = e } @@ -144,12 +144,12 @@ async function debuggableNavigatorLock( } console.error( - `Waited for over 2s to acquire an Auth client lock`, + `Waited for over 10s to acquire an Auth client lock`, await navigator.locks.query(), stackException ) })() - }, 2000) + }, 10000) try { return await navigatorLock(name, acquireTimeout, async () => { @@ -175,13 +175,27 @@ async function debuggableNavigatorLock( } } +// Wrap fetch with 30-second timeout to prevent indefinite hangs +const fetchWithTimeout: typeof fetch = async (input, init) => { + const timeoutSignal = AbortSignal.timeout(30000) // 30 seconds + const existingSignal = init?.signal + const combinedSignal = existingSignal + ? AbortSignal.any([existingSignal, timeoutSignal]) + : timeoutSignal + + return fetch(input, { + ...init, + signal: combinedSignal, + }) +} + export const gotrueClient = new AuthClient({ url: process.env.NEXT_PUBLIC_GOTRUE_URL, storageKey: STORAGE_KEY, detectSessionInUrl: shouldDetectSessionInUrl, debug: debug ? (persistedDebug ? logIndexedDB : true) : false, lock: navigatorLockEnabled ? debuggableNavigatorLock : undefined, - + fetch: fetchWithTimeout, ...('localStorage' in globalThis ? { storage: globalThis.localStorage, userStorage: globalThis.localStorage } : null), diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 497ec44f9de42..5d699a3d4c884 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -239,7 +239,7 @@ export interface ProjectCreationSimpleVersionSubmittedEvent { * the instance size selected in the project creation form */ properties: { - instanceSize: string + instanceSize?: string } groups: TelemetryGroups } @@ -257,7 +257,7 @@ export interface ProjectCreationSimpleVersionConfirmModalOpenedEvent { * the instance size selected in the project creation form */ properties: { - instanceSize: string + instanceSize?: string } groups: Omit } diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index 76d95a9201060..eb8c2c207c2fc 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -36,6 +36,7 @@ allow_list = [ "[Cc]onsecutiveness", "[Cc]ooldowns?", "[Cc]oroutines?", + "ComposeAuth", "[Cc]ron", "[Cc]rypto", "[Cc]ryptography",