diff --git a/apps/docs/content/guides/auth/sessions.mdx b/apps/docs/content/guides/auth/sessions.mdx index 1b79d9f8d6644..c7eac35d2bf9e 100644 --- a/apps/docs/content/guides/auth/sessions.mdx +++ b/apps/docs/content/guides/auth/sessions.mdx @@ -69,7 +69,7 @@ Otherwise sessions are progressively deleted from the database 24 hours after th ### What are recommended values for access token (JWT) expiration? -Most applications should use the default expiration time of 1 hour. This can be customized in your project's [Auth settings](/dashboard/project/_/settings/jwt) in the Advanced Settings section. +Most applications should use the default expiration time of 1 hour. This can be customized in your [project's settings](/dashboard/project/_/settings/jwt/legacy) in the JWT Keys > Legacy JWT Secret section. Setting a value over 1 hour is generally discouraged for security reasons, but it may make sense in certain situations. diff --git a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx index ef13b111f2c75..2e749fc4da22a 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx @@ -39,3 +39,5 @@ You can update scopes of your OAuth app at any time, but existing OAuth app user | `Rest` | `Write` | Update a project's PostgREST configuration | | `Secrets` | `Read` | Retrieve a project's API keys
Retrieve a project's secrets
Retrieve a project's pgsodium config | | `Secrets` | `Write` | Create or update a project's secrets
Update a project's pgsodium configuration | +| `Storage` | `Read` | Retrieve a project's storage buckets. | +| | diff --git a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx index dac00f80c61ed..fef7ebb55fcae 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/log-drains.mdx @@ -36,7 +36,7 @@ Usage is shown as "Log Drain Hours" on your invoice. ### Pricing -Log Drains are available as a project Add-On for all Team and Enterprise users. Each Log Drain costs per hour ( per month). +Log Drains are available as a project Add-On for all Pro, Team and Enterprise users. Each Log Drain costs per hour ( per month). ## Log Drain Events diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/telemetry/log-drains.mdx index 9bf926f643d13..9b5b8adfdd09c 100644 --- a/apps/docs/content/guides/telemetry/log-drains.mdx +++ b/apps/docs/content/guides/telemetry/log-drains.mdx @@ -4,7 +4,7 @@ title: 'Log Drains' description: 'Getting started with Supabase Log Drains' --- -Log drains will send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Team and Enterprise Plans. Log drains is available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains). +Log drains send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Pro, Team and Enterprise Plans. Log drains are available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains). You can read about the initial announcement [here](/blog/log-drains) and vote for your preferred drains in [this discussion](https://github.com/orgs/supabase/discussions/28324?sort=top). diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index c8a6dbae32391..b588725932521 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -4,6 +4,7 @@ Supabase is 100% remote. Aaron Byrne Ada Wong +Adam Mohammed Adam Mokan AJ Matias Akash Manimaran @@ -82,6 +83,7 @@ Francesco Sansalvadore Garrett Crowell Gerardo Estaba Gildas Garcia +Giuseppe Mandato Greg Kress Greg P Greg Richardson @@ -109,6 +111,7 @@ Jim Brodeur Jim Chanco Jr Joakim Ahrlin Joel Low +Joel Martin John Pena John Schaeffer Jon M diff --git a/apps/studio/components/grid/components/editor/TimeEditor.tsx b/apps/studio/components/grid/components/editor/TimeEditor.tsx index ba9a34fdcab1f..ca89fb895d8ea 100644 --- a/apps/studio/components/grid/components/editor/TimeEditor.tsx +++ b/apps/studio/components/grid/components/editor/TimeEditor.tsx @@ -1,7 +1,7 @@ -import * as React from 'react' -import type { RenderEditCellProps } from 'react-data-grid' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' +import * as React from 'react' +import type { RenderEditCellProps } from 'react-data-grid' dayjs.extend(customParseFormat) @@ -29,15 +29,18 @@ function BaseEditor({ onClose, }: TimeEditorProps) { const value = row[column.key as keyof TRow] as unknown as string - const timeValue = value ? dayjs(value, format).format(INPUT_TIME_FORMAT) : value function onChange(event: React.ChangeEvent) { const _value = event.target.value + if (_value == '') { onRowChange({ ...row, [column.key]: null }) } else { - const _timeValue = dayjs(_value, INPUT_TIME_FORMAT).format(format) - onRowChange({ ...row, [column.key]: _timeValue }) + const dayJsValue = dayjs(_value, format) + if (dayJsValue.isValid()) { + const _timeValue = dayJsValue.format(format) + onRowChange({ ...row, [column.key]: _timeValue }) + } } } @@ -45,7 +48,7 @@ function BaseEditor({ onClose(true)} type="time" diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 106f6ea8d1559..2c4c5f7a2bab1 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -29,6 +29,7 @@ import { QueueOperationsPreview } from './QueueOperationsPreview' import { TableFilterBarPreview } from './TableFilterBarPreview' import { UnifiedLogsPreview } from './UnifiedLogsPreview' import { useFeaturePreviews } from './useFeaturePreviews' +import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' const FEATURE_PREVIEW_KEY_TO_CONTENT: { [key: string]: ReactNode @@ -56,6 +57,11 @@ export const FeaturePreviewModal = () => { const featurePreviewContext = useFeaturePreviewContext() const { mutate: sendEvent } = useSendEventMutation() + const [isDismissedTableFilterBar, setIsDismissedTableFilterBar] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), + false + ) + const { flags, onUpdateFlag } = featurePreviewContext const selectedFeature = featurePreviews.find((preview) => preview.key === selectedFeatureKey) ?? featurePreviews[0] @@ -72,6 +78,13 @@ export const FeaturePreviewModal = () => { properties: { feature: selectedFeature.key }, groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) + + if ( + selectedFeature.key === LOCAL_STORAGE_KEYS.UI_PREVIEW_TABLE_FILTER_BAR && + !isDismissedTableFilterBar + ) { + setIsDismissedTableFilterBar(true) + } } return ( diff --git a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx index f1c0c2f3940a4..9899cd98fd2be 100644 --- a/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PausedState/ProjectPausedState.tsx @@ -236,23 +236,9 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { {isPauseStatusSuccess && !isRestoreDisabled && ( - {isFreePlan ? ( - - ) : ( - - )} - { > Resume project + + {isFreePlan ? ( + + ) : ( + + )} )} diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx index d2b622044a4fe..3ed0b57989c44 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorLayout.tsx @@ -6,6 +6,7 @@ import { PropsWithChildren, useEffect } from 'react' import { ProjectLayoutWithAuth } from '../ProjectLayout' import { SaveQueueActionBar } from '@/components/grid/components/footer/operations/SaveQueueActionBar' +import { useIsTableFilterBarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { BannerTableEditorFilter } from '@/components/ui/BannerStack/Banners/BannerTableEditorFilter' import { useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' @@ -15,6 +16,7 @@ const TABLE_EDITOR_NEW_FILTER_BANNER_ID = 'table-editor-new-filter-banner' export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { const { ref } = useParams() const { addBanner, dismissBanner } = useBannerStack() + const isTableFilterBarEnabled = useIsTableFilterBarEnabled() const [isTableEditorNewFilterBannerDismissed] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.TABLE_EDITOR_NEW_FILTER_BANNER_DISMISSED(ref ?? ''), @@ -29,7 +31,7 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { useEffect(() => { if (!isPermissionsLoaded) return - if (canReadTables && !isTableEditorNewFilterBannerDismissed) { + if (canReadTables && !isTableEditorNewFilterBannerDismissed && !isTableFilterBarEnabled) { addBanner({ id: TABLE_EDITOR_NEW_FILTER_BANNER_ID, priority: 2, @@ -49,6 +51,7 @@ export const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => { canReadTables, isPermissionsLoaded, isTableEditorNewFilterBannerDismissed, + isTableFilterBarEnabled, ]) if (isPermissionsLoaded && !canReadTables) { diff --git a/apps/studio/data/auth/user-update-mutation.ts b/apps/studio/data/auth/user-update-mutation.ts index 986a2416a1e95..2c50c82485577 100644 --- a/apps/studio/data/auth/user-update-mutation.ts +++ b/apps/studio/data/auth/user-update-mutation.ts @@ -1,8 +1,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' - import { handleError, patch } from 'data/fetchers' +import { toast } from 'sonner' import type { ResponseError, UseCustomMutationOptions } from 'types' + import { authKeys } from './keys' export type UserUpdateVariables = { @@ -37,8 +37,11 @@ export const useUserUpdateMutation = ({ return useMutation({ mutationFn: (vars) => updateUser(vars), async onSuccess(data, variables, context) { - const { projectRef } = variables - await queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) }) + const { projectRef, userId } = variables + await Promise.all([ + queryClient.invalidateQueries({ queryKey: authKeys.usersInfinite(projectRef) }), + queryClient.invalidateQueries({ queryKey: authKeys.user(projectRef, userId) }), + ]) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/www/_go/events/stripe-exec-dinner-thank-you.tsx b/apps/www/_go/events/stripe-exec-dinner-thank-you.tsx new file mode 100644 index 0000000000000..073bfb5ee405a --- /dev/null +++ b/apps/www/_go/events/stripe-exec-dinner-thank-you.tsx @@ -0,0 +1,34 @@ +import type { GoPageInput } from 'marketing' +import Link from 'next/link' +import { Button } from 'ui' + +const page: GoPageInput = { + template: 'thank-you', + slug: 'stripe/exec-dinner/thank-you', + metadata: { + title: "You're confirmed | Supabase Executive Dinner", + description: + 'Your RSVP for the Supabase executive dinner at Spruce on April 29, 2026 has been confirmed.', + }, + hero: { + title: "You're confirmed", + description: + "We'll send details and directions closer to the date. We look forward to seeing you at Spruce on April 29.", + }, + sections: [ + { + type: 'single-column', + title: 'In the meantime', + description: 'Learn more about what we are building at Supabase.', + children: ( +
+ +
+ ), + }, + ], +} + +export default page diff --git a/apps/www/_go/events/stripe-exec-dinner.tsx b/apps/www/_go/events/stripe-exec-dinner.tsx new file mode 100644 index 0000000000000..d215de0af7dce --- /dev/null +++ b/apps/www/_go/events/stripe-exec-dinner.tsx @@ -0,0 +1,134 @@ +import type { GoPageInput } from 'marketing' + +const page: GoPageInput = { + template: 'lead-gen', + slug: 'stripe/exec-dinner', + metadata: { + title: 'Executive Dinner: The Future of Scalable Databases | Supabase', + description: + 'Join Supabase leaders for an intimate dinner exploring what comes next for Postgres at scale. April 29, 2026 at Spruce, San Francisco.', + }, + hero: { + title: 'The future of scalable databases', + subtitle: 'An intimate executive dinner hosted by Supabase', + description: + 'Join Supabase product and engineering leaders for a dinner conversation about where Postgres is headed -- from scaling beyond single-node limits to managing globally distributed workloads. Expect sharp perspectives, good food, and the opportunity to connect with other engineering leaders.', + ctas: [ + { + label: 'Reserve your seat', + href: '#rsvp', + variant: 'primary', + }, + ], + }, + sections: [ + { + type: 'single-column', + title: 'Details', + children: ( +
+

Spruce Restaurant

+

3640 Sacramento St, San Francisco, CA

+

Tuesday, April 29, 2026

+

6:30 PM -- Cocktails and introductions

+

7:00 PM -- Dinner and discussion

+
+ ), + }, + { + type: 'single-column', + title: 'Your hosts', + children: ( +
+
+
+

Nate Asp

+

VP, Supabase

+
+
+

Deepthi Sigireddi

+

+ Head of Databases, Supabase +

+
+
+

Sugu Sougoumarane

+

+ Head of Multigres, Supabase +

+
+
+
+ ), + }, + { + type: 'form', + id: 'rsvp', + title: 'Reserve your seat', + description: "Space is limited. Let us know you're coming.", + fields: [ + { + type: 'text', + name: 'first_name', + label: 'First Name', + placeholder: 'First Name', + required: true, + half: true, + }, + { + type: 'text', + name: 'last_name', + label: 'Last Name', + placeholder: 'Last Name', + required: true, + half: true, + }, + { + type: 'email', + name: 'email_address', + label: 'Email', + placeholder: 'Work email', + required: true, + }, + { + type: 'text', + name: 'company_name', + label: 'Company', + placeholder: 'Company name', + required: true, + }, + ], + submitLabel: 'Confirm RSVP', + successRedirect: '/go/stripe/exec-dinner/thank-you', + disclaimer: + 'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy).', + crm: { + hubspot: { + formGuid: 'eb135982-73b5-4701-a3e9-909564107087', + fieldMap: { + first_name: 'firstname', + last_name: 'lastname', + email_address: 'email', + company_name: 'company', + }, + consent: + 'By submitting this form, I confirm that I have read and understood the Privacy Policy.', + }, + customerio: { + event: 'event_registered', + profileMap: { + email_address: 'email', + first_name: 'first_name', + last_name: 'last_name', + company_name: 'company_name', + }, + staticProperties: { + event_name: 'Stripe Sessions 2026 Exec Dinner', + }, + }, + }, + }, + ], +} + +export default page diff --git a/apps/www/_go/events/stripe-sessions-contest.tsx b/apps/www/_go/events/stripe-sessions-contest.tsx new file mode 100644 index 0000000000000..66333279be98c --- /dev/null +++ b/apps/www/_go/events/stripe-sessions-contest.tsx @@ -0,0 +1,60 @@ +import type { GoPageInput } from 'marketing' +import Link from 'next/link' +import { Button } from 'ui' + +const page: GoPageInput = { + template: 'lead-gen', + slug: 'stripe/contest', + metadata: { + title: 'Win an iPhone 17 Pro Max | Supabase at Stripe Sessions', + description: + 'Create a Supabase account and load data for a 1-in-10 chance to win an iPhone 17 Pro Max. Stripe Sessions 2026.', + }, + hero: { + title: 'Win an iPhone 17 Pro Max', + subtitle: 'Supabase at Stripe Sessions 2026', + description: + "Great meeting you at Stripe Sessions. Try Supabase if you haven't already -- it's Postgres with all the tools you need to build AI-native applications. We're running a sweepstakes and you have a 1-in-10 chance of winning. Those are better odds than anywhere else!", + image: { + src: '/images/landing-pages/stripe-sessions/iphone17-pro-max.png', + alt: 'Orange iPhone 17 Pro Max', + width: 400, + height: 500, + }, + ctas: [ + { + label: 'Get started', + href: '#how-to-enter', + variant: 'primary', + }, + ], + }, + sections: [ + { + type: 'single-column', + id: 'how-to-enter', + title: 'How to enter', + children: ( +
+
    +
  1. Create a Supabase account with your email address
  2. +
  3. Load data into a Supabase database
  4. +
  5. Complete these steps by Monday, May 11, 2026 at 12:00 PM PST
  6. +
+ +

+ No purchase necessary. Void where prohibited.{' '} + + Official rules + + . +

+
+ ), + }, + ], +} + +export default page diff --git a/apps/www/_go/index.tsx b/apps/www/_go/index.tsx index 5c020232551c3..6411bfab7dbd4 100644 --- a/apps/www/_go/index.tsx +++ b/apps/www/_go/index.tsx @@ -1,19 +1,21 @@ import type { GoPageInput } from 'marketing' import byocEarlyAccess from './pre-release/byoc-early-access' -import exampleLeadGen from './lead-gen/example-lead-gen' -import exampleLegal from './legal/example-legal' -import exampleThankYou from './thank-you/example-thank-you' import boltWebinar from './webinar/bolt-webinar' import boltWebinarThankYou from './webinar/bolt-webinar-thank-you' +import contestRules from './legal/contest-rules' +import stripeExecDinner from './events/stripe-exec-dinner' +import stripeExecDinnerThankYou from './events/stripe-exec-dinner-thank-you' +import stripeSessionsContest from './events/stripe-sessions-contest' const pages: GoPageInput[] = [ byocEarlyAccess, - exampleLeadGen, - exampleThankYou, - exampleLegal, - boltWebinar, - boltWebinarThankYou, + contestRules, + boltWebinar, // remove after March 31, 2026 + boltWebinarThankYou, // remove after March 31, 2026 + stripeExecDinner, + stripeExecDinnerThankYou, + stripeSessionsContest, ] export default pages diff --git a/apps/www/_go/legal/contest-rules.tsx b/apps/www/_go/legal/contest-rules.tsx new file mode 100644 index 0000000000000..6f58932cc5586 --- /dev/null +++ b/apps/www/_go/legal/contest-rules.tsx @@ -0,0 +1,150 @@ +import type { GoPageInput } from 'marketing' + +const page: GoPageInput = { + template: 'legal', + slug: 'contest-rules', + metadata: { + title: 'Supabase Official Rules For Giveaways & Sweepstakes', + description: 'Official rules for Supabase giveaways, sweepstakes, and promotional contests.', + }, + hero: { + title: 'Supabase Official Rules For Giveaways & Sweepstakes', + }, + effectiveDate: '2026-02-16', + body: ` + NO PURCHASE OR PAYMENT NECESSARY TO ENTER OR WIN. A PURCHASE OR PAYMENT WILL NOT INCREASE YOUR CHANCES OF WINNING. VOID WHERE PROHIBITED. + + ## 1. Sponsor + + Supabase, Inc. ("Sponsor"). + + ## 2. Promotion Period + + The Promotion begins at the time and date announced at the applicable Supabase event or digital campaign and ends at the time specified in the Promotion materials (the "Promotion Period"). Sponsor's system is the official time-keeping device. + + ## 3. Eligibility + + The Promotion is open to legal residents of countries where such promotions are permitted by law, who are at least the age of majority in their jurisdiction of residence at the time of entry. + + The following are not eligible to enter or win: + + - Employees, officers, and directors of Sponsor and its affiliates + - Contractors, marketing agencies, and vendors involved in administering the Promotion + - Immediate family members and household members of the above + + The Promotion is void in jurisdictions where sweepstakes are prohibited or materially restricted without filings, bonding, or government approvals, including but not limited to: + + - Brazil + - Quebec (Canada) + - Italy + - Spain + - Russia + - Iran + - North Korea + - Syria + - Cuba + - Belarus + - Mainland China + - Crimea, Donetsk, and Luhansk regions of Ukraine + - Any jurisdiction subject to U.S., EU, or UN trade sanctions + - Any jurisdiction requiring registration, bonding, or regulatory approvals not obtained by Sponsor + + Sponsor reserves the right to disqualify any entrant whose participation is unlawful or non-compliant under local law. + + ## 4. How to Enter + + To enter, follow the instructions provided at the applicable Supabase event or promotional campaign, which may include: + + - Scanning a badge at a Supabase booth + - Submitting an entry form + - Creating a Supabase account + - Completing a product action (e.g., launching a project, loading data) + + Limit: One (1) entry per person unless otherwise specified. + + Entries must be received during the Promotion Period. + + Automated, robotic, or scripted entries are prohibited. + + ## 5. Prizes + + Prizes will be described in the Promotion materials and may include consumer electronics, merchandise, credits, or other items of value. Approximate Retail Value ("ARV") will be disclosed for each Promotion. Sponsor reserves the right to substitute a prize of equal or greater value if the advertised prize becomes unavailable or cannot be legally shipped or awarded in a winner's jurisdiction. + + ## 6. Winner Selection & Notification + + Winners will be selected in a random drawing or other announced method at the time specified in the Promotion materials. Winners will be notified via the contact information provided at entry. Failure to respond within seven (7) days of notification may result in forfeiture, and an alternate winner may be selected. + + ## 7. International Winners -- Taxes, Customs, and Duties + + Winners are solely responsible for all taxes, customs duties, VAT, import fees, brokerage fees, and any other charges imposed by local authorities. Sponsor may require winners to complete tax forms or declarations as required by law. Sponsor will not be responsible for prizes delayed, seized, or rejected by customs or local authorities. If a prize cannot be legally delivered to a winner's country, Sponsor may substitute a prize of equal or greater value or award a cash or credit equivalent, at Sponsor's discretion. + + ## 8. Prize Substitution & Local Compliance + + Sponsor reserves the right to: + + - Substitute prizes based on local availability or legal restrictions + - Modify the prize format (e.g., cash, store credit, digital gift card) for international winners + - Disqualify entries from jurisdictions where the Promotion cannot be lawfully administered + + ## 9. Odds + + Odds of winning depend on the number of eligible entries received. + + ## 10. Publicity + + Except where prohibited by law, participation constitutes winner's consent to Sponsor's use of winner's name, likeness, city, country, and prize information for promotional purposes without additional compensation. + + ## 11. Privacy + + Personal information collected in connection with the Promotion will be used in accordance with Sponsor's [Privacy Policy](https://supabase.com/privacy). + + ## 12. Limitation of Liability + + Sponsor is not responsible for: + + - Lost, late, incomplete, or misdirected entries + - Technical failures of any kind + - Unauthorized human intervention + - Errors in the administration of the Promotion + + By entering, entrants release and hold harmless Sponsor from any claims arising out of participation or prize acceptance. + + ## 13. Disputes & Governing Law + + All disputes shall be governed by the laws of the State of California, USA, without regard to conflict-of-law principles. Any dispute, claim, or controversy arising out of or relating to this Promotion shall be resolved by binding arbitration administered by the American Arbitration Association under its Commercial Arbitration Rules. All claims shall be resolved individually, without class actions. The arbitration shall be conducted in San Francisco, California. The arbitrator's decision shall be final and binding. + + ## 14. Sanctions & Trade Compliance + + Entrants represent and warrant that they are not located in, under the control of, or a resident of any country or territory subject to U.S., EU, or UN sanctions and are not listed on any government sanctions or restricted party lists. + + Sponsor reserves the right to disqualify any entrant or withhold any prize if awarding such prize would violate applicable trade or sanctions laws. + + ## 15. Data Transfers -- GDPR / UK GDPR + + By entering, entrants consent to the transfer, processing, and storage of their personal data in the United States and other jurisdictions for purposes of administering the Promotion, selecting winners, awarding prizes, and related promotional activities. + + ## 16. Force Majeure / Regulatory Impossibility + + Sponsor shall not be responsible for any failure or delay in performance due to events beyond its reasonable control, including but not limited to acts of God, war, terrorism, labor disputes, governmental orders, regulatory changes, public health emergencies, shipping disruptions, or technical failures. + + Sponsor reserves the right to modify, suspend, or cancel the Promotion if regulatory changes or governmental restrictions make lawful administration impracticable. + + ## 17. General Conditions + + Sponsor reserves the right to cancel, modify, or suspend the Promotion if fraud, technical failure, or any other factor impairs the integrity of the Promotion. Sponsor's decisions are final. + + ## 18. Winners List + + For a copy of the winners list, send a request to: + + [legal@supabase.com](mailto:legal@supabase.com) + + within sixty (60) days of the end of the Promotion. + + © 2026 Supabase Inc. + + [Privacy Policy](https://supabase.com/privacy) · [Terms of Service](https://supabase.com/terms) +`, +} + +export default page diff --git a/apps/www/public/images/landing-pages/stripe-sessions/iphone17-pro-max.png b/apps/www/public/images/landing-pages/stripe-sessions/iphone17-pro-max.png new file mode 100644 index 0000000000000..e05f951d1e136 Binary files /dev/null and b/apps/www/public/images/landing-pages/stripe-sessions/iphone17-pro-max.png differ diff --git a/docker/.env.example b/docker/.env.example index 5fcff319982f9..3e1e2dc35e647 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -217,3 +217,22 @@ KONG_HTTPS_PORT=8443 # Enable webp support IMGPROXY_ENABLE_WEBP_DETECTION=true + + +############ +# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt +############ + +# Documentation: +# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https + +# Usage: +# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d +# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d + +# Domain name for the proxy (must point to your server) +PROXY_DOMAIN=your-domain.example.com + +# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN). +# This should be a valid email, not a placehoder (otherwise Certbot may fail to start). +CERTBOT_EMAIL=admin@example.com diff --git a/docker/docker-compose.caddy.yml b/docker/docker-compose.caddy.yml new file mode 100644 index 0000000000000..b31ee8db586fa --- /dev/null +++ b/docker/docker-compose.caddy.yml @@ -0,0 +1,34 @@ +services: + + kong: + ports: !reset [] + + caddy: + container_name: supabase-caddy + image: caddy:2 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + depends_on: + kong: + condition: service_healthy + environment: + PROXY_DOMAIN: ${PROXY_DOMAIN} + PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME} + PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD} + command: + - /bin/sh + - -c + - | + PROXY_AUTH_PASSWORD=$$(caddy hash-password --plaintext "$$PROXY_AUTH_PASSWORD") && \ + caddy run --config /etc/caddy/Caddyfile --adapter caddyfile + volumes: + - ./volumes/proxy/caddy:/etc/caddy + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: diff --git a/docker/docker-compose.nginx.yml b/docker/docker-compose.nginx.yml new file mode 100644 index 0000000000000..e693a86aee171 --- /dev/null +++ b/docker/docker-compose.nginx.yml @@ -0,0 +1,33 @@ +services: + + kong: + ports: !reset [] + + nginx: + container_name: supabase-nginx + image: jonasal/nginx-certbot:6.0.1-nginx1.29.5 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + depends_on: + kong: + condition: service_healthy + environment: + PROXY_DOMAIN: ${PROXY_DOMAIN} + CERTBOT_EMAIL: ${CERTBOT_EMAIL} + PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME} + PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD} + command: + - /bin/bash + - -c + - | + printf '%s:%s\n' "$${PROXY_AUTH_USERNAME}" "$$(openssl passwd -apr1 "$${PROXY_AUTH_PASSWORD}")" > /etc/nginx/user_conf.d/dashboard-passwd && \ + envsubst '$${PROXY_DOMAIN}' < /etc/nginx/supabase-nginx.conf.tpl > /etc/nginx/user_conf.d/nginx.conf && \ + /scripts/start_nginx_certbot.sh + volumes: + - ./volumes/proxy/nginx/supabase-nginx.conf.tpl:/etc/nginx/supabase-nginx.conf.tpl:ro + - nginx_letsencrypt:/etc/letsencrypt + +volumes: + nginx_letsencrypt: diff --git a/docker/volumes/proxy/caddy/Caddyfile b/docker/volumes/proxy/caddy/Caddyfile new file mode 100644 index 0000000000000..d76c3ed36f3dd --- /dev/null +++ b/docker/volumes/proxy/caddy/Caddyfile @@ -0,0 +1,36 @@ +{$PROXY_DOMAIN} { + @supabase_api path /auth/v1/* /rest/v1/* /graphql/v1/* /realtime/v1/* /functions/v1/* /mcp + + handle @supabase_api { + reverse_proxy kong:8000 + } + + handle_path /storage/v1/* { + # CORS headers for Storage (bypasses Kong, which normally handles CORS) + @cors_preflight method OPTIONS + handle @cors_preflight { + header Access-Control-Allow-Origin * + header Access-Control-Allow-Methods "GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS" + header Access-Control-Allow-Headers * + respond 204 + } + + header Access-Control-Allow-Origin * + + reverse_proxy storage:5000 { + # Required for TUS resumable upload Location headers and S3 signature verification. + header_up X-Forwarded-Prefix /{http.request.orig_uri.path.0}/{http.request.orig_uri.path.1} + } + } + + handle { + basic_auth { + # PROXY_AUTH_PASSWORD is overwritten with the bcrypt on startup + {$PROXY_AUTH_USERNAME} {$PROXY_AUTH_PASSWORD} + } + + reverse_proxy studio:3000 + } + + header -server +} diff --git a/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl b/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl new file mode 100644 index 0000000000000..f50f23653c026 --- /dev/null +++ b/docker/volumes/proxy/nginx/supabase-nginx.conf.tpl @@ -0,0 +1,109 @@ +upstream kong_upstream { + server kong:8000; + keepalive 2; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name ${PROXY_DOMAIN}; + server_tokens off; + + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + ssl_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${PROXY_DOMAIN}/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/${PROXY_DOMAIN}/chain.pem; + + ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Prevent 502 errors from large Supabase auth cookies + large_client_header_buffers 4 16k; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + + location / { + auth_basic "supabase"; + auth_basic_user_file /etc/nginx/user_conf.d/dashboard-passwd; + + proxy_pass http://studio:3000; + } + + location /auth { + proxy_pass http://kong_upstream; + } + + location /rest { + proxy_pass http://kong_upstream; + } + + location /realtime/v1/ { + proxy_pass http://kong_upstream; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 3600s; + } + + location /storage/v1/ { + proxy_pass http://storage:5000/; + proxy_buffering off; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + + # Required for TUS resumable upload Location headers and S3 signature verification. + proxy_set_header X-Forwarded-Prefix /storage/v1; + + client_max_body_size 0; + + # CORS headers for Storage (bypasses Kong, which normally handles CORS) + if ($request_method = OPTIONS) { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' '*'; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + return 204; + } + + add_header 'Access-Control-Allow-Origin' '*'; + } + + location /functions { + proxy_pass http://kong_upstream; + } + + location /graphql { + proxy_pass http://kong_upstream; + } + + location /mcp { + proxy_pass http://kong_upstream; + } +} diff --git a/packages/marketing/src/go/actions/submitForm.ts b/packages/marketing/src/go/actions/submitForm.ts index b44abfff91049..f9d37ea58ef41 100644 --- a/packages/marketing/src/go/actions/submitForm.ts +++ b/packages/marketing/src/go/actions/submitForm.ts @@ -130,7 +130,9 @@ export async function submitFormAction( context, consent, event: crm.customerio?.event, - properties: crm.customerio ? (values as Record) : undefined, + properties: crm.customerio + ? { ...(values as Record), ...crm.customerio.staticProperties } + : undefined, customerioProfile, } as any) diff --git a/packages/marketing/src/go/schemas.ts b/packages/marketing/src/go/schemas.ts index 540adff4efd9c..8009df88e5ac3 100644 --- a/packages/marketing/src/go/schemas.ts +++ b/packages/marketing/src/go/schemas.ts @@ -163,6 +163,12 @@ export const customerioFormConfigSchema = z.object({ * Example: { workEmail: 'email', firstName: 'first_name' } */ profileMap: z.record(z.string(), z.string()).optional(), + /** + * Static properties merged into the Customer.io track() event payload. + * Use this for fixed values that aren't form fields (e.g. event_name, source). + * Example: { event_name: 'Stripe Sessions 2026 Exec Dinner' } + */ + staticProperties: z.record(z.string(), z.unknown()).optional(), }) export const formCrmConfigSchema = z diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts index 0b906209837c8..b32860d63c559 100644 --- a/packages/shared-data/plans.ts +++ b/packages/shared-data/plans.ts @@ -62,6 +62,7 @@ export const plans: PricingInformation[] = [ 'Email support', 'Daily backups stored for 7 days', '7-day log retention', + ['Add Log Drains', 'additional $60 per drain, per project'], ], preface: 'Everything in the Free Plan, plus:', cta: 'Get Started', @@ -85,7 +86,6 @@ export const plans: PricingInformation[] = [ 'Priority email support & SLAs', 'Daily backups stored for 14 days', '28-day log retention', - ['Add Log Drains', 'additional $60 per drain, per project'], ], preface: 'Everything in the Pro Plan, plus:', cta: 'Get Started', diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index 495798f6e3863..f6ff197a0cbf3 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -590,7 +590,7 @@ export const pricing: Pricing = { title: 'Log Drain', plans: { free: false, - pro: false, + pro: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], team: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], enterprise: 'Custom', }, diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index da1a0cc3c448f..7d7bdf8dd6e3d 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -51,6 +51,7 @@ allow_list = [ "[Dd]ata[Ff]rames?", "[Dd]atasets?", "[Dd]atasources?", + "[Dd]atetime", "[Dd]e facto", "[Dd]enylists?", "[Dd]evs?", @@ -103,6 +104,7 @@ allow_list = [ "[Oo]vercommit(s|ted|ting)?", "[Pp]arallelization", "[Pp]arams?", + "[Pp]assthrough", "[Pp]laintext", "[Pp]olyfill(s|ed)?", "[Pp]oolers?", @@ -137,6 +139,7 @@ allow_list = [ "[Tt]radeoffs?", "[Tt]unneled", "UncaughtException", + "[Uu]ncomment(ing|ed)?", "[Uu]nlink(ing|s|ed)?", "[Uu]pserts?", "[Uu]ptime", @@ -161,8 +164,10 @@ allow_list = [ "BotFather", "Brevo", "bytea", + "[Cc]addy", "CAPTCHA", "Cartes Bancaires", + "[Cc]ertbot", "ChatGPT", "Citus", "ClickHouse", @@ -210,6 +215,7 @@ allow_list = [ "GraphQL", "Groonga", "HackerOne", + "[Hh][Aa][Pp]roxy", "HashiCorp", "Heroku", "Homebrew", @@ -218,6 +224,7 @@ allow_list = [ "HypoPG", "IdP", "ImageMagick", + "imgproxy", "Inbucket", "Inferencer", "Infisical", @@ -249,6 +256,7 @@ allow_list = [ "Mailtrap", "Mansueli", "Metabase", + "[Mm]in[Ii][Oo]", "Mixpeek", "Multiplatform", "MySQL", @@ -256,6 +264,7 @@ allow_list = [ "Nano", "Netlify", "Next.js", + "[Nn]ginx", "NoSQL", "Node.js", "Nuxt", @@ -297,6 +306,7 @@ allow_list = [ "PrivateLink", "PyIceberg", "Qodo", + "rclone", "README", "Redis", "RedwoodJS", @@ -313,15 +323,17 @@ allow_list = [ "SecureStore", "SendGrid", "Session Timebox", + "Snapchat", "Snaplet", "Solana", "SolidJS", "Spotify", "Sqitch", + "Supabase", "Supavisor", "SvelteKit", "SwiftUI", - "Supabase", + "Reddit", "Remapper", "TablePlus", "TextLocal", @@ -329,6 +341,7 @@ allow_list = [ "TimescaleDB", "tmux", "TooManyChannels", + "[Tt]raefik", "Transformers.js", "tsquery", "Twilio",