From 56c34d20f8b0074455b864e080f5238dc587d89c Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Tue, 26 May 2026 10:17:00 -0400 Subject: [PATCH] feat(www): wire up State of Startups 2026 results page (#45231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature — ports real 2026 survey data and narrative into the existing `apps/www/app/state-of-startups` page. ## What is the current behavior? The 2026 page renders the pre-launch `` signup form (`SHOW_RESULTS = false`), and the 2026 chart components still query `responses_2025` via the unsuffixed 2025 RPCs. The chapter narrative in `state-of-startups-2026.tsx` is placeholder copy cloned from 2025, and there are no charts for any of the new-in-2026 survey questions (auth provider, share of AI-generated code, paid AI/product subscriptions, agent building, MCP adoption). ## What is the new behavior? - Flips `SHOW_RESULTS` to `true` so `/state-of-startups` renders ``. - Repoints the 14 carry-over chart components to `responses_2026` and calls `_2026`-suffixed RPC siblings (the original 2025 RPCs stay intact so the 2025 page can live at its own URL). - Adds 5 new chart components — `AuthProviderChart`, `AICodebasePercentChart`, `PaidSubscriptionsChart`, `BuildingAgentsChart`, `MCPAdoptionChart` — and wires them into `SurveyChapterSection`'s chart registry. - Rewrites `state-of-startups-2026.tsx` into 8 chapters with real 2026 narrative, pull quotes, and summarized answers from the source-of-truth `supabase/state-of-startups` repo. - Adds `.context/**` to `.prettierignore` so the scratch directory holding the companion SQL migrations is ignored. ## Additional context Backend changes live in the separate `supabase/state-of-startups` repo / project `iddgenoqmqztnnefcbwp`: a `responses_2026` table, 5 new 2026-only RPCs, and 14 `_2026`-suffixed siblings of the existing RPCs (drafts staged under `.context/sos-2026-survey-sql/` locally, not in this PR). This PR's chart fetches will error until those migrations are applied and `2026-responses.arrayified.csv` is loaded. ## Summary by CodeRabbit * **New Features** * Released State of Startups 2026 results with a year selector, preloaded results cache for faster viewing, and hourly revalidation. * Added new interactive charts (AI codebase share, auth providers, biggest challenges, building agents, MCP adoption, paid subscriptions, location) and updated many charts for 2026; charts now show “added in 2026” when not available. * **UX** * Reworked page layout: floating table of contents relocated, year toggle added, and animations simplified for smoother transitions. * **Documentation** * Restructured survey narrative into new thematic chapters, revised chapter content, stats, and visuals. * **Chores** * Updated formatting ignore rules to skip generated/context files. --- .prettierignore | 1 + .../StateOfStartups2026Content.tsx | 175 ++-- .../components/SurveyChapterSection.tsx | 138 ++- .../components/SurveyChart.tsx | 302 +++--- .../components/SurveyStatCard.tsx | 178 +++- .../components/YearToggle.tsx | 43 + .../charts/AICodebasePercentChart.tsx | 33 + .../components/charts/AICodingToolsChart.tsx | 4 +- .../components/charts/AIModelsChart.tsx | 4 +- .../charts/AcceleratorParticipationChart.tsx | 4 +- .../components/charts/AuthProviderChart.tsx | 48 + .../charts/BiggestChallengeChart.tsx | 4 +- .../components/charts/BuildingAgentsChart.tsx | 26 + .../components/charts/DatabasesChart.tsx | 4 +- .../components/charts/FundingStageChart.tsx | 4 +- .../components/charts/IndustryChart.tsx | 4 +- .../charts/InitialPayingCustomersChart.tsx | 4 +- .../components/charts/LocationChart.tsx | 4 +- .../components/charts/MCPAdoptionChart.tsx | 26 + .../components/charts/NewIdeasChart.tsx | 4 +- .../charts/PaidSubscriptionsChart.tsx | 48 + .../charts/RegularSocialMediaUseChart.tsx | 4 +- .../components/charts/RoleChart.tsx | 4 +- .../components/charts/SalesToolsChart.tsx | 4 +- .../components/charts/WorldOutlookChart.tsx | 4 +- .../components/survey-data-context.tsx | 141 +++ .../components/year-context.tsx | 37 + .../lib/preload-survey-data.ts | 215 +++++ apps/www/app/state-of-startups/page.tsx | 20 +- .../data/surveys/state-of-startups-2026.tsx | 895 +++++++++++++----- 30 files changed, 1762 insertions(+), 620 deletions(-) create mode 100644 apps/www/app/state-of-startups/components/YearToggle.tsx create mode 100644 apps/www/app/state-of-startups/components/charts/AICodebasePercentChart.tsx create mode 100644 apps/www/app/state-of-startups/components/charts/AuthProviderChart.tsx create mode 100644 apps/www/app/state-of-startups/components/charts/BuildingAgentsChart.tsx create mode 100644 apps/www/app/state-of-startups/components/charts/MCPAdoptionChart.tsx create mode 100644 apps/www/app/state-of-startups/components/charts/PaidSubscriptionsChart.tsx create mode 100644 apps/www/app/state-of-startups/components/survey-data-context.tsx create mode 100644 apps/www/app/state-of-startups/components/year-context.tsx create mode 100644 apps/www/app/state-of-startups/lib/preload-survey-data.ts diff --git a/.prettierignore b/.prettierignore index e1ba10c8806..46c5219234d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ node_modules pnpm-lock.yaml docker* apps/**/out +.context/** # prettier-plugin-sql-cst only supports sqlite syntax **/supabase/migrations/*.sql apps/www/schema.sql diff --git a/apps/www/app/state-of-startups/StateOfStartups2026Content.tsx b/apps/www/app/state-of-startups/StateOfStartups2026Content.tsx index a8dcde34920..e6f9ab1b691 100644 --- a/apps/www/app/state-of-startups/StateOfStartups2026Content.tsx +++ b/apps/www/app/state-of-startups/StateOfStartups2026Content.tsx @@ -9,9 +9,16 @@ import { forwardRef, useEffect, useRef, useState, type RefObject } from 'react' import { Button, cn } from 'ui' import { StateOfStartupsAuroraHeader } from './components/StateOfStartupsAuroraHeader' +import { + SurveyDataProvider, + type SurveyDataCache, + type SurveyStatCache, +} from './components/survey-data-context' import { SurveyChapter } from './components/SurveyChapter' import { SurveyChapterSection } from './components/SurveyChapterSection' import { SurveySectionBreak } from './components/SurveySectionBreak' +import { YearProvider } from './components/year-context' +import { YearToggle } from './components/YearToggle' interface FloatingTocProps { tocRef: RefObject @@ -29,15 +36,8 @@ function FloatingTableOfContents({ const currentChapter = pageData.pageChapters[activeChapter - 1] return ( - -
+ + {!isTocOpen ? ( )} -
+
) } -export default function StateOfStartups2026Content() { +export default function StateOfStartups2026Content({ + preloadedData, + preloadedStats, +}: { + preloadedData: SurveyDataCache + preloadedStats: SurveyStatCache +}) { const [showFloatingToc, setShowFloatingToc] = useState(false) const [isTocOpen, setIsTocOpen] = useState(false) const [activeChapter, setActiveChapter] = useState(1) @@ -192,75 +198,92 @@ export default function StateOfStartups2026Content() { }, [isTocOpen]) return ( - - - {showFloatingToc && ( - - )} - - {/* Intro section */} -
- - -
- {/* Intro text */} -
-

{pageData.heroSection.subheader}

-

{pageData.heroSection.cta}

-
+ + + + + {showFloatingToc && ( + + + + + + + )} + + {/* Intro section */} +
+ + +
+ {/* Intro text */} +
+

{pageData.heroSection.subheader}

+

{pageData.heroSection.cta}

+
- {/* Table of contents */} -
    - {pageData.pageChapters.map((chapter, chapterIndex) => ( -
  1. - - - {chapterIndex + 1} - {' '} - {chapter.shortTitle} - -
  2. - ))} -
-
+ {/* Table of contents */} +
    + {pageData.pageChapters.map((chapter, chapterIndex) => ( +
  1. + + + {chapterIndex + 1} + {' '} + {chapter.shortTitle} + +
  2. + ))} +
+
- -
+ + - {pageData.pageChapters.map((chapter, chapterIndex) => ( - - {chapter.sections.map((section, sectionIndex) => ( - + {pageData.pageChapters.map((chapter, chapterIndex) => ( + + {chapter.sections.map((section, sectionIndex) => ( + + ))} + ))} - - ))} - - -
+ + + + + ) } diff --git a/apps/www/app/state-of-startups/components/SurveyChapterSection.tsx b/apps/www/app/state-of-startups/components/SurveyChapterSection.tsx index 6d86ac72c20..97a2e6ffb23 100644 --- a/apps/www/app/state-of-startups/components/SurveyChapterSection.tsx +++ b/apps/www/app/state-of-startups/components/SurveyChapterSection.tsx @@ -1,30 +1,36 @@ 'use client' import { useAccent } from './accent-context' -import { SurveyStatCard } from './SurveyStatCard' -import { SurveyWordCloud } from './SurveyWordCloud' -import { SurveySummarizedAnswer } from './SurveySummarizedAnswer' +import { AcceleratorParticipationChart } from './charts/AcceleratorParticipationChart' +import { AICodebasePercentChart } from './charts/AICodebasePercentChart' +import { AICodingToolsChart } from './charts/AICodingToolsChart' +import { AIModelsChart } from './charts/AIModelsChart' +import { AuthProviderChart } from './charts/AuthProviderChart' +import { BiggestChallengeChart } from './charts/BiggestChallengeChart' +import { BuildingAgentsChart } from './charts/BuildingAgentsChart' +import { DatabasesChart } from './charts/DatabasesChart' +import { FundingStageChart } from './charts/FundingStageChart' +import { IndustryChart } from './charts/IndustryChart' +import { InitialPayingCustomersChart } from './charts/InitialPayingCustomersChart' +import { LocationChart } from './charts/LocationChart' +import { MCPAdoptionChart } from './charts/MCPAdoptionChart' +import { NewIdeasChart } from './charts/NewIdeasChart' +import { PaidSubscriptionsChart } from './charts/PaidSubscriptionsChart' +import { RegularSocialMediaUseChart } from './charts/RegularSocialMediaUseChart' +import { RoleChart } from './charts/RoleChart' +import { SalesToolsChart } from './charts/SalesToolsChart' +import { WorldOutlookChart } from './charts/WorldOutlookChart' import { SurveyRankedAnswersPair } from './SurveyRankedAnswersPair' import { SurveySectionBreak } from './SurveySectionBreak' -import { AcceleratorParticipationChart } from './charts/AcceleratorParticipationChart' -import { RoleChart } from './charts/RoleChart' -import { IndustryChart } from './charts/IndustryChart' -import { FundingStageChart } from './charts/FundingStageChart' -import { DatabasesChart } from './charts/DatabasesChart' -import { AIModelsChart } from './charts/AIModelsChart' -import { LocationChart } from './charts/LocationChart' -import { SalesToolsChart } from './charts/SalesToolsChart' -import { AICodingToolsChart } from './charts/AICodingToolsChart' -import { RegularSocialMediaUseChart } from './charts/RegularSocialMediaUseChart' -import { NewIdeasChart } from './charts/NewIdeasChart' -import { InitialPayingCustomersChart } from './charts/InitialPayingCustomersChart' -import { WorldOutlookChart } from './charts/WorldOutlookChart' -import { BiggestChallengeChart } from './charts/BiggestChallengeChart' +import { SurveyStatCard, type SurveyStatSource } from './SurveyStatCard' +import { SurveySummarizedAnswer } from './SurveySummarizedAnswer' +import { SurveyWordCloud } from './SurveyWordCloud' +import { useYear, type SurveyYear } from './year-context' interface SurveyChapterSectionProps { title: string description: string - stats?: Array<{ percent: number; label: string }> + stats?: Array<{ percent: number; label: string; source?: SurveyStatSource }> charts?: string[] wordCloud?: { @@ -36,6 +42,13 @@ interface SurveyChapterSectionProps { answers: string[] } rankedAnswersPair?: Array<{ label: string; answers: string[] }> + /** + * Set when an entire section's data first appeared in a given survey year. + * If the user toggles the page to an earlier year, the section body is + * replaced with a "Not available in " placeholder while the + * title and description still render. + */ + newInYear?: SurveyYear children?: React.ReactNode } @@ -47,27 +60,35 @@ export function SurveyChapterSection({ wordCloud, summarizedAnswer, rankedAnswersPair, + newInYear, children, }: SurveyChapterSectionProps) { const accent = useAccent() + const { year } = useYear() + const isAvailableForYear = !newInYear || year >= newInYear const eyebrowColor = 'text-brand-link dark:text-brand' const chartComponents = { - RoleChart, - IndustryChart, - FundingStageChart, AcceleratorParticipationChart, - DatabasesChart, + AICodebasePercentChart, AICodingToolsChart, AIModelsChart, - RegularSocialMediaUseChart, - NewIdeasChart, + AuthProviderChart, + BiggestChallengeChart, + BuildingAgentsChart, + DatabasesChart, + FundingStageChart, + IndustryChart, InitialPayingCustomersChart, + LocationChart, + MCPAdoptionChart, + NewIdeasChart, + PaidSubscriptionsChart, + RegularSocialMediaUseChart, + RoleChart, SalesToolsChart, WorldOutlookChart, - BiggestChallengeChart, - LocationChart, } return ( @@ -84,31 +105,48 @@ export function SurveyChapterSection({

- {stats && ( - + {isAvailableForYear ? ( + <> + {stats && ( + + )} + + {charts?.map((chartName, index) => { + const ChartComponent = chartComponents[chartName as keyof typeof chartComponents] + return ChartComponent ? : null + })} + + {rankedAnswersPair && } + + {wordCloud && } + + {summarizedAnswer && ( + + )} + + {children} + + ) : ( +
+

Not available in {year}.

+

+ This question was added to the {newInYear} survey. Switch the year toggle to{' '} + {newInYear} to view. +

+
)} - - {charts?.map((chartName, index) => { - const ChartComponent = chartComponents[chartName as keyof typeof chartComponents] - return ChartComponent ? : null - })} - - {rankedAnswersPair && } - - {wordCloud && } - - {summarizedAnswer && ( - - )} - - {children} diff --git a/apps/www/app/state-of-startups/components/SurveyChart.tsx b/apps/www/app/state-of-startups/components/SurveyChart.tsx index 390ae8dc8df..2127edd0f66 100644 --- a/apps/www/app/state-of-startups/components/SurveyChart.tsx +++ b/apps/www/app/state-of-startups/components/SurveyChart.tsx @@ -1,7 +1,9 @@ 'use client' -import { useEffect, useState, useRef, useCallback } from 'react' -import { createClient } from '@supabase/supabase-js' + +import CodeBlock from '~/components/CodeBlock/CodeBlock' import { motion } from 'framer-motion' +import { ChevronsUpDown } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Button, DropdownMenu, @@ -9,16 +11,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from 'ui' -import { ChevronsUpDown } from 'lucide-react' -import TwoOptionToggle from './TwoOptionToggle' -import CodeBlock from '~/components/CodeBlock/CodeBlock' -import { useAccent } from './accent-context' -// Separate Supabase client for survey project -const externalSupabase = createClient( - process.env.NEXT_PUBLIC_SURVEY_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY! -) +import { useAccent } from './accent-context' +import { useSurveyDataCache, type ChartDataItem } from './survey-data-context' +import TwoOptionToggle from './TwoOptionToggle' +import { rpcNameForYear, useYear, type SurveyYear } from './year-context' // Sentinel for “no filter” const NO_FILTER = 'unset' @@ -37,12 +34,6 @@ interface Filters { [key: string]: FilterConfig } -interface ChartDataItem { - label: string - value: number - rawValue: number -} - interface FilterColumnConfig { label: string options: string[] @@ -98,81 +89,22 @@ function useFilterOptions(filterColumns: string[]) { return { filters } } -// Fetch survey data using secure database functions -function useSurveyData( - shouldFetch: boolean, - functionName: string, - functionParams: (activeFilters: Record) => Record, - activeFilters: Record -) { - const [chartData, setChartData] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - if (!shouldFetch) return - - async function fetchData() { - try { - setIsLoading(true) - setError(null) - - let data, fetchError - - const functionParamsData = functionParams(activeFilters) - const { data: functionData, error: functionError } = await externalSupabase.rpc( - functionName, - functionParamsData - ) - data = functionData - fetchError = functionError - - if (fetchError) { - console.error('Error executing SQL query:', fetchError) - setError(fetchError.message) - return - } - - // Calculate total for percentage calculation - const total = data.reduce( - (sum: number, row: any) => sum + parseInt(row.count || row.total), - 0 - ) - - // Transform the data to match chart format - const processedData = data.map((row: any) => { - const count = parseInt(row.count || row.total) - const rawPercentage = total > 0 ? (count / total) * 100 : 0 - const roundedPercentage = Math.round(rawPercentage) - - return { - label: row.label || row.value || row[Object.keys(row)[0]], // Get the first column as label - value: roundedPercentage, - rawValue: rawPercentage, // Keep the raw value for bar scaling - } - }) - - setChartData(processedData) - } catch (err: any) { - console.error('Error in fetchData:', err) - setError(err.message) - } finally { - setIsLoading(false) - } - } - - fetchData() - }, [shouldFetch, functionName, functionParams, activeFilters]) - - return { chartData, isLoading, error } -} - interface SurveyChartProps { title: string targetColumn: string filterColumns: string[] generateSQLQuery?: (activeFilters: Record) => string + /** + * Base RPC name without a year suffix (e.g. `get_role_stats`). The chart + * appends `_` based on the active year context. + */ functionName: string + /** + * Year in which this question was first asked. When the user views an + * earlier year, the chart shows a "new question" empty state instead of + * fetching. + */ + newInYear?: SurveyYear } export function SurveyChart({ @@ -181,48 +113,17 @@ export function SurveyChart({ filterColumns, generateSQLQuery, functionName, + newInYear, }: SurveyChartProps) { + const { year } = useYear() + const isAvailableForYear = !newInYear || year >= newInYear + const resolvedFunctionName = rpcNameForYear(functionName, year) + const { get, fetchAndCache } = useSurveyDataCache() const accent = useAccent() const accentBg = 'hsl(var(--brand-300))' const accentBarFg = 'bg-brand' const accentBarText = 'text-brand-link dark:text-brand' - const [isInView, setIsInView] = useState(false) - const chartRef = useRef(null) - const [shouldAnimateBars, setShouldAnimateBars] = useState(false) - const [hasLoadedOnce, setHasLoadedOnce] = useState(false) - - // Intersection observer to trigger chart data loading via database function - useEffect(() => { - const chartRefCurrent = chartRef.current - - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting && !hasLoadedOnce) { - setIsInView(true) - setHasLoadedOnce(true) - observer.disconnect() // Only trigger once - } - }) - }, - { - threshold: 0.1, - rootMargin: '128px', // Start loading before the component comes into view - } - ) - - if (chartRefCurrent) { - observer.observe(chartRefCurrent) - } - - return () => { - if (chartRefCurrent) { - observer.unobserve(chartRefCurrent) - } - } - }, [hasLoadedOnce]) - // Each chart uses a subset of available filters, defined below const { filters } = useFilterOptions(filterColumns) @@ -235,49 +136,69 @@ export function SurveyChart({ ) // Build function parameters based on filterColumns - const buildFunctionParams = useCallback((activeFilters: Record) => { + const buildFunctionParams = useCallback((filterValues: Record) => { const params: Record = {} - - // Convert single values to arrays for the function parameters - if (activeFilters.person_age && activeFilters.person_age !== 'unset') { - params.person_age_filter = [activeFilters.person_age] + if (filterValues.person_age && filterValues.person_age !== NO_FILTER) { + params.person_age_filter = [filterValues.person_age] } - if (activeFilters.location && activeFilters.location !== 'unset') { - params.location_filter = [activeFilters.location] + if (filterValues.location && filterValues.location !== NO_FILTER) { + params.location_filter = [filterValues.location] } - if (activeFilters.money_raised && activeFilters.money_raised !== 'unset') { - params.money_raised_filter = [activeFilters.money_raised] + if (filterValues.money_raised && filterValues.money_raised !== NO_FILTER) { + params.money_raised_filter = [filterValues.money_raised] } - if (activeFilters.team_size && activeFilters.team_size !== 'unset') { - params.team_size_filter = [activeFilters.team_size] + if (filterValues.team_size && filterValues.team_size !== NO_FILTER) { + params.team_size_filter = [filterValues.team_size] } - return params }, []) - // Use the custom hook to fetch data - const { - chartData, - isLoading: dataLoading, - error: dataError, - } = useSurveyData(isInView, functionName, buildFunctionParams, activeFilters) + const params = useMemo( + () => buildFunctionParams(activeFilters), + [activeFilters, buildFunctionParams] + ) - // Reset animation state when filters change + const cached = isAvailableForYear ? get(resolvedFunctionName, params) : undefined + const [chartData, setChartData] = useState(cached ?? []) + const [isLoading, setIsLoading] = useState(!cached && isAvailableForYear) + const [dataError, setDataError] = useState(null) + + // Sync data with cache + on-demand fetch when year/filters change useEffect(() => { - setShouldAnimateBars(false) - }, [activeFilters]) - - // Trigger bar animation when data loads - useEffect(() => { - if (!dataLoading && chartData.length > 0 && !shouldAnimateBars) { - // Small delay to ensure DOM is ready - const timer = setTimeout(() => { - setShouldAnimateBars(true) - }, 100) - - return () => clearTimeout(timer) + if (!isAvailableForYear) { + setChartData([]) + setIsLoading(false) + setDataError(null) + return } - }, [dataLoading, chartData.length, shouldAnimateBars]) + + const next = get(resolvedFunctionName, params) + if (next) { + setChartData(next) + setIsLoading(false) + setDataError(null) + return + } + + let cancelled = false + setIsLoading(true) + setDataError(null) + fetchAndCache(resolvedFunctionName, params) + .then((data) => { + if (cancelled) return + setChartData(data) + setIsLoading(false) + }) + .catch((err: any) => { + if (cancelled) return + setDataError(err?.message ?? 'Error fetching data') + setIsLoading(false) + }) + + return () => { + cancelled = true + } + }, [resolvedFunctionName, params, isAvailableForYear, get, fetchAndCache]) const [view, setView] = useState<'chart' | 'sql'>('chart') @@ -305,17 +226,8 @@ export function SurveyChart({ const BUTTON_AREA_HEIGHT = 40 // px const CHART_HEIGHT = FIXED_HEIGHT - BUTTON_AREA_HEIGHT // px - const skeletonData = [ - { label: 'Loading', value: 0, rawValue: 0 }, - { label: 'Loading', value: 0, rawValue: 0 }, - { label: 'Loading', value: 0, rawValue: 0 }, - ] - - const displayData = dataLoading ? skeletonData : chartData || [] - return (
- {dataError ? ( + {!isAvailableForYear ? ( +
+

+ This question was added to the {newInYear} survey. +

+

+ Switch the year to {newInYear} to view results. +

+
+ ) : dataError ? (

Error: {dataError}

) : view === 'chart' ? ( -
+
{chartData.length > 0 ? (
- {displayData.map((item, index) => ( -
+ {chartData.map((item, index) => ( +
{/* Text above the bar */}
{item.label} @@ -413,11 +337,9 @@ export function SurveyChart({ {/* Filled portion of the bar */}
{/* Foreground pattern for the filled portion */} @@ -466,29 +388,25 @@ export function SurveyChart({ {generateSQLQuery(activeFilters)} ) : ( - {`// Function call: ${functionName}(${JSON.stringify(buildFunctionParams(activeFilters), null, 2)})`} + {`// Function call: ${resolvedFunctionName}(${JSON.stringify(buildFunctionParams(activeFilters), null, 2)})`} )}
)} {/* Expand button overlay - only show for chart view */} - {view === 'chart' && - !isExpanded && - !dataLoading && - !dataError && - chartData.length > 3 && ( -
- -
- )} + {view === 'chart' && !isExpanded && !dataError && chartData.length > 3 && ( +
+ +
+ )}
diff --git a/apps/www/app/state-of-startups/components/SurveyStatCard.tsx b/apps/www/app/state-of-startups/components/SurveyStatCard.tsx index 83070e4b23e..0c1f88e544d 100644 --- a/apps/www/app/state-of-startups/components/SurveyStatCard.tsx +++ b/apps/www/app/state-of-startups/components/SurveyStatCard.tsx @@ -1,23 +1,85 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' + import { useAccent } from './accent-context' +import { useSurveyDataCache } from './survey-data-context' +import { useYear } from './year-context' const ANIMATION_DURATION = 600 -// These values are calculated via static SQL queries under 'Key Stats' and rounded for display in data/surveys/state-of-startups-2025.tsx file -export function SurveyStatCard({ label, percent }: { label: string; percent: number }) { - const [displayValue, setDisplayValue] = useState(0) - const [hasAnimated, setHasAnimated] = useState(false) - const [shouldAnimateBar, setShouldAnimateBar] = useState(false) +// Stats are written against the 2026 narrative. If a stat declares a +// `source` ({ column, aggregation, target }), the card looks up the live +// percent for the active year in the preloaded stat cache. Otherwise it +// falls back to the static `percent` for 2026 and "—" for other years. +const NARRATIVE_YEAR = 2026 + +export interface SurveyStatSource { + column: string + /** + * 'single' | 'multi' | 'boolean'. Kept as `string` here so narrative + * literals don't need `as const` casts; preload-survey-data.ts dispatches + * on the literal value when calling the matching generic RPC. + */ + aggregation: string + /** Bucket label(s) to match in the cached stat rows. Sums when multiple. */ + target: string | string[] +} + +interface SurveyStatCardProps { + label: string + percent: number + source?: SurveyStatSource +} + +export function SurveyStatCard({ label, percent, source }: SurveyStatCardProps) { const cardRef = useRef(null) const accent = useAccent() + const { year } = useYear() + const { getStat } = useSurveyDataCache() const accentBg = 'bg-brand' const accentText = 'text-brand' + // Resolve the percent to display: live value when a source is declared and + // present in the cache, otherwise fall back to the static 2026 baseline + // (with "—" on prior years). + const resolvedPercent: number | null = useMemo(() => { + if (source) { + const data = getStat(year, source.column) + if (data && data.respondentCount > 0) { + const targets = Array.isArray(source.target) ? source.target : [source.target] + let count = 0 + let matched = 0 + for (const row of data.rows) { + if (targets.includes(row.label)) { + count += row.count + matched += 1 + } + } + if (matched > 0) { + return Math.round((count / data.respondentCount) * 100) + } + // Cache present but target not in this year's rows → option didn't + // exist that year. Render "—". + return null + } + // No cache entry for this (year, column) — column likely didn't exist + // that year, or the preload errored. Fall through to the static + // fallback so the 2026 page still shows something pre-deploy. + } + return year === NARRATIVE_YEAR ? percent : null + }, [source, year, getStat, percent]) + + const [displayValue, setDisplayValue] = useState(0) + const [hasAnimated, setHasAnimated] = useState(false) + const [shouldAnimateBar, setShouldAnimateBar] = useState(false) + + // Initial scroll-in animation: count up from 0 to the resolved value the + // first time the card enters the viewport. useEffect(() => { - if (!percent || hasAnimated) return + if (hasAnimated) return + if (resolvedPercent === null) return const observer = new IntersectionObserver( (entries) => { @@ -26,25 +88,16 @@ export function SurveyStatCard({ label, percent }: { label: string; percent: num setHasAnimated(true) setShouldAnimateBar(true) - // Start counting animation const startTime = Date.now() - const duration = ANIMATION_DURATION const startValue = 0 - const endValue = percent + const endValue = resolvedPercent const animate = () => { const elapsed = Date.now() - startTime - const progress = Math.min(elapsed / duration, 1) - - // Easing function for smooth animation + const progress = Math.min(elapsed / ANIMATION_DURATION, 1) const easeOutQuart = 1 - Math.pow(1 - progress, 4) - const currentValue = Math.round(startValue + (endValue - startValue) * easeOutQuart) - - setDisplayValue(currentValue) - - if (progress < 1) { - requestAnimationFrame(animate) - } + setDisplayValue(Math.round(startValue + (endValue - startValue) * easeOutQuart)) + if (progress < 1) requestAnimationFrame(animate) } requestAnimationFrame(animate) @@ -55,16 +108,27 @@ export function SurveyStatCard({ label, percent }: { label: string; percent: num ) const currentCardRef = cardRef.current - if (currentCardRef) { - observer.observe(currentCardRef) - } - + if (currentCardRef) observer.observe(currentCardRef) return () => { - if (currentCardRef) { - observer.unobserve(currentCardRef) - } + if (currentCardRef) observer.unobserve(currentCardRef) } - }, [percent, hasAnimated]) + }, [resolvedPercent, hasAnimated]) + + // After the initial animation, snap to the latest resolved value when the + // year toggle (or live data) changes. + useEffect(() => { + if (!hasAnimated) return + if (resolvedPercent === null) { + setDisplayValue(0) + setShouldAnimateBar(false) + return + } + setDisplayValue(resolvedPercent) + setShouldAnimateBar(true) + }, [resolvedPercent, hasAnimated]) + + const hasValue = resolvedPercent !== null + const renderedBarValue = hasValue ? resolvedPercent : 0 return (
@@ -73,7 +137,7 @@ export function SurveyStatCard({ label, percent }: { label: string; percent: num className="h-2 relative overflow-hidden" style={ { - '--bar-value': percent, + '--bar-value': renderedBarValue, } as React.CSSProperties } > @@ -88,35 +152,45 @@ export function SurveyStatCard({ label, percent }: { label: string; percent: num }} /> - {/* Filled portion of the bar */} -
- {/* Foreground pattern for the filled portion */} + {/* Filled portion of the bar (only when there is a value to show) */} + {hasValue && (
-
+ > +
+
+ )}
{/* Text */}
-

- {displayValue} - % -

+ {hasValue ? ( +

+ {displayValue} + % +

+ ) : ( +

+ — +

+ )}

{label}

diff --git a/apps/www/app/state-of-startups/components/YearToggle.tsx b/apps/www/app/state-of-startups/components/YearToggle.tsx new file mode 100644 index 00000000000..8de3ca3317f --- /dev/null +++ b/apps/www/app/state-of-startups/components/YearToggle.tsx @@ -0,0 +1,43 @@ +'use client' + +import { motion } from 'framer-motion' +import { cn } from 'ui' + +import { SURVEY_YEARS, useYear, type SurveyYear } from './year-context' + +export function YearToggle({ className }: { className?: string }) { + const { year, setYear } = useYear() + + return ( + + {SURVEY_YEARS.map((option) => { + const active = option === year + return ( + + ) + })} + + ) +} diff --git a/apps/www/app/state-of-startups/components/charts/AICodebasePercentChart.tsx b/apps/www/app/state-of-startups/components/charts/AICodebasePercentChart.tsx new file mode 100644 index 00000000000..a2b3a78f9f7 --- /dev/null +++ b/apps/www/app/state-of-startups/components/charts/AICodebasePercentChart.tsx @@ -0,0 +1,33 @@ +import { buildWhereClause, SurveyChart } from '../SurveyChart' + +function generateAICodebasePercentSQL(activeFilters: Record) { + const whereClause = buildWhereClause(activeFilters, ['ai_generated_codebase_percent IS NOT NULL']) + + return `SELECT + ai_generated_codebase_percent AS band, + COUNT(*) AS total +FROM responses_2026 +${whereClause} +GROUP BY ai_generated_codebase_percent +ORDER BY CASE + WHEN ai_generated_codebase_percent = '0%' THEN 1 + WHEN ai_generated_codebase_percent = '1-25%' THEN 2 + WHEN ai_generated_codebase_percent = '26-50%' THEN 3 + WHEN ai_generated_codebase_percent = '51-75%' THEN 4 + WHEN ai_generated_codebase_percent = '76-100%' THEN 5 + ELSE 6 + END;` +} + +export function AICodebasePercentChart() { + return ( + + ) +} diff --git a/apps/www/app/state-of-startups/components/charts/AICodingToolsChart.tsx b/apps/www/app/state-of-startups/components/charts/AICodingToolsChart.tsx index 66e8acc83f8..4908714d640 100644 --- a/apps/www/app/state-of-startups/components/charts/AICodingToolsChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/AICodingToolsChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateAICodingToolsSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -24,7 +24,7 @@ function generateAICodingToolsSQL(activeFilters: Record) { END AS technology_clean FROM ( SELECT id, unnest(ai_coding_tools) AS technology - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/AIModelsChart.tsx b/apps/www/app/state-of-startups/components/charts/AIModelsChart.tsx index a7dbee1367c..897bbd1a3a9 100644 --- a/apps/www/app/state-of-startups/components/charts/AIModelsChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/AIModelsChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateAIModelsSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -23,7 +23,7 @@ function generateAIModelsSQL(activeFilters: Record) { END AS technology_clean FROM ( SELECT id, unnest(ai_models_used) AS technology - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/AcceleratorParticipationChart.tsx b/apps/www/app/state-of-startups/components/charts/AcceleratorParticipationChart.tsx index 6ee816aaf80..88a9e027824 100644 --- a/apps/www/app/state-of-startups/components/charts/AcceleratorParticipationChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/AcceleratorParticipationChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateAcceleratorParticipationSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters, ['accelerator_participation IS NOT NULL']) @@ -11,7 +11,7 @@ function generateAcceleratorParticipationSQL(activeFilters: Record) { + const whereClause = buildWhereClause(activeFilters) + + return `WITH auth_provider_mapping AS ( + SELECT + id, + CASE + WHEN provider IN ( + 'Supabase Auth', + 'Auth0', + 'Clerk', + 'NextAuth / Auth.js', + 'Firebase Auth', + 'AWS Cognito', + 'Stytch', + 'WorkOS', + 'Rolled our own' + ) THEN provider + ELSE 'Other' + END AS provider_clean + FROM ( + SELECT id, unnest(auth_provider) AS provider + FROM responses_2026 + ${whereClause} + ) sub + ) + SELECT + provider_clean AS provider, + COUNT(DISTINCT id) AS total + FROM auth_provider_mapping + GROUP BY provider_clean + ORDER BY total DESC;` +} + +export function AuthProviderChart() { + return ( + + ) +} diff --git a/apps/www/app/state-of-startups/components/charts/BiggestChallengeChart.tsx b/apps/www/app/state-of-startups/components/charts/BiggestChallengeChart.tsx index 4a549ac61db..2337f098906 100644 --- a/apps/www/app/state-of-startups/components/charts/BiggestChallengeChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/BiggestChallengeChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateBiggestChallengeSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters, ['biggest_challenge IS NOT NULL']) @@ -10,7 +10,7 @@ function generateBiggestChallengeSQL(activeFilters: Record) { WHEN biggest_challenge IN ('Customer acquisition', 'Technical complexity', 'Product-market fit', 'Product-market fit', 'Fundraising', 'Hiring', 'Other') THEN biggest_challenge ELSE 'Other' END AS biggest_challenge_clean - FROM responses_2025 + FROM responses_2026 ${whereClause} ) SELECT diff --git a/apps/www/app/state-of-startups/components/charts/BuildingAgentsChart.tsx b/apps/www/app/state-of-startups/components/charts/BuildingAgentsChart.tsx new file mode 100644 index 00000000000..c278d1f848a --- /dev/null +++ b/apps/www/app/state-of-startups/components/charts/BuildingAgentsChart.tsx @@ -0,0 +1,26 @@ +import { buildWhereClause, SurveyChart } from '../SurveyChart' + +function generateBuildingAgentsSQL(activeFilters: Record) { + const whereClause = buildWhereClause(activeFilters, ['building_ai_agents IS NOT NULL']) + + return `SELECT + building_ai_agents AS answer, + COUNT(*) AS total +FROM responses_2026 +${whereClause} +GROUP BY building_ai_agents +ORDER BY total DESC;` +} + +export function BuildingAgentsChart() { + return ( + + ) +} diff --git a/apps/www/app/state-of-startups/components/charts/DatabasesChart.tsx b/apps/www/app/state-of-startups/components/charts/DatabasesChart.tsx index e78e0f11f96..060f0f8a1bd 100644 --- a/apps/www/app/state-of-startups/components/charts/DatabasesChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/DatabasesChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateDatabasesSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -20,7 +20,7 @@ function generateDatabasesSQL(activeFilters: Record) { END AS technology_clean FROM ( SELECT id, unnest(databases) AS technology - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/FundingStageChart.tsx b/apps/www/app/state-of-startups/components/charts/FundingStageChart.tsx index fef480e165f..ddec045d03d 100644 --- a/apps/www/app/state-of-startups/components/charts/FundingStageChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/FundingStageChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateFundingStageSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -6,7 +6,7 @@ function generateFundingStageSQL(activeFilters: Record) { return `SELECT funding_stage, COUNT(*) AS total - FROM responses_2025${whereClause ? '\n' + whereClause : ''} + FROM responses_2026${whereClause ? '\n' + whereClause : ''} GROUP BY funding_stage ORDER BY CASE WHEN funding_stage = 'Bootstrapped' THEN 1 diff --git a/apps/www/app/state-of-startups/components/charts/IndustryChart.tsx b/apps/www/app/state-of-startups/components/charts/IndustryChart.tsx index 15d4d46df22..bc69b2fa076 100644 --- a/apps/www/app/state-of-startups/components/charts/IndustryChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/IndustryChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateIndustrySQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters, ['industry IS NOT NULL']) @@ -12,7 +12,7 @@ function generateIndustrySQL(activeFilters: Record) { WHEN industry IN ('SaaS', 'Dev tools', 'AI / ML', 'Consumer', 'Education', 'eCommerce', 'Fintech', 'Healthtech') THEN industry ELSE 'Other' END AS industry_clean - FROM responses_2025 + FROM responses_2026 ${whereClause} ) SELECT diff --git a/apps/www/app/state-of-startups/components/charts/InitialPayingCustomersChart.tsx b/apps/www/app/state-of-startups/components/charts/InitialPayingCustomersChart.tsx index 35cdeb5a365..f60dd398832 100644 --- a/apps/www/app/state-of-startups/components/charts/InitialPayingCustomersChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/InitialPayingCustomersChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateInitialPayingCustomersSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -21,7 +21,7 @@ function generateInitialPayingCustomersSQL(activeFilters: Record END AS source_clean FROM ( SELECT id, unnest(initial_paying_customers) AS source - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/LocationChart.tsx b/apps/www/app/state-of-startups/components/charts/LocationChart.tsx index 2bec94cf5db..877233d7acb 100644 --- a/apps/www/app/state-of-startups/components/charts/LocationChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/LocationChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateLocationSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -6,7 +6,7 @@ function generateLocationSQL(activeFilters: Record) { return `SELECT location, COUNT(*) AS total -FROM responses_2025${whereClause ? '\n' + whereClause : ''} +FROM responses_2026${whereClause ? '\n' + whereClause : ''} GROUP BY location ORDER BY total DESC;` } diff --git a/apps/www/app/state-of-startups/components/charts/MCPAdoptionChart.tsx b/apps/www/app/state-of-startups/components/charts/MCPAdoptionChart.tsx new file mode 100644 index 00000000000..eddd45e4855 --- /dev/null +++ b/apps/www/app/state-of-startups/components/charts/MCPAdoptionChart.tsx @@ -0,0 +1,26 @@ +import { buildWhereClause, SurveyChart } from '../SurveyChart' + +function generateMCPAdoptionSQL(activeFilters: Record) { + const whereClause = buildWhereClause(activeFilters, ['mcp_adoption IS NOT NULL']) + + return `SELECT + mcp_adoption AS answer, + COUNT(*) AS total +FROM responses_2026 +${whereClause} +GROUP BY mcp_adoption +ORDER BY total DESC;` +} + +export function MCPAdoptionChart() { + return ( + + ) +} diff --git a/apps/www/app/state-of-startups/components/charts/NewIdeasChart.tsx b/apps/www/app/state-of-startups/components/charts/NewIdeasChart.tsx index 6be14f42f1e..da8ebc33666 100644 --- a/apps/www/app/state-of-startups/components/charts/NewIdeasChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/NewIdeasChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateNewIdeasSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -23,7 +23,7 @@ function generateNewIdeasSQL(activeFilters: Record) { END AS avenue_clean FROM ( SELECT id, unnest(new_ideas) AS avenue - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/PaidSubscriptionsChart.tsx b/apps/www/app/state-of-startups/components/charts/PaidSubscriptionsChart.tsx new file mode 100644 index 00000000000..c7a3af2f1db --- /dev/null +++ b/apps/www/app/state-of-startups/components/charts/PaidSubscriptionsChart.tsx @@ -0,0 +1,48 @@ +import { buildWhereClause, SurveyChart } from '../SurveyChart' + +function generatePaidSubscriptionsSQL(activeFilters: Record) { + const whereClause = buildWhereClause(activeFilters) + + return `WITH subscriptions_mapping AS ( + SELECT + id, + CASE + WHEN subscription IN ( + 'Claude', + 'OpenAI / ChatGPT', + 'Cursor', + 'GitHub Copilot', + 'Gemini', + 'Lovable', + 'Perplexity', + 'Vercel', + 'Supabase' + ) THEN subscription + ELSE 'Other' + END AS subscription_clean + FROM ( + SELECT id, unnest(subscriptions) AS subscription + FROM responses_2026 + ${whereClause} + ) sub + ) + SELECT + subscription_clean AS subscription, + COUNT(DISTINCT id) AS total + FROM subscriptions_mapping + GROUP BY subscription_clean + ORDER BY total DESC;` +} + +export function PaidSubscriptionsChart() { + return ( + + ) +} diff --git a/apps/www/app/state-of-startups/components/charts/RegularSocialMediaUseChart.tsx b/apps/www/app/state-of-startups/components/charts/RegularSocialMediaUseChart.tsx index fd4395bd629..8a8751b994b 100644 --- a/apps/www/app/state-of-startups/components/charts/RegularSocialMediaUseChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/RegularSocialMediaUseChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateRegularSocialMediaUseSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -24,7 +24,7 @@ function generateRegularSocialMediaUseSQL(activeFilters: Record) END AS platform_clean FROM ( SELECT id, unnest(regular_social_media_use) AS platform - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/RoleChart.tsx b/apps/www/app/state-of-startups/components/charts/RoleChart.tsx index 6ed52273489..a0d9b80a6be 100644 --- a/apps/www/app/state-of-startups/components/charts/RoleChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/RoleChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateRoleSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -10,7 +10,7 @@ function generateRoleSQL(activeFilters: Record) { ELSE 'Other' END AS role, COUNT(*) AS total -FROM responses_2025${whereClause ? '\n' + whereClause : ''} +FROM responses_2026${whereClause ? '\n' + whereClause : ''} GROUP BY CASE WHEN role = 'Founder / Co-founder' THEN 'Founder' WHEN role IN ('Engineer', 'Founder / Co-founder') THEN role diff --git a/apps/www/app/state-of-startups/components/charts/SalesToolsChart.tsx b/apps/www/app/state-of-startups/components/charts/SalesToolsChart.tsx index c65d9ba91d2..3e155a814dc 100644 --- a/apps/www/app/state-of-startups/components/charts/SalesToolsChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/SalesToolsChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateSalesToolsSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -20,7 +20,7 @@ function generateSalesToolsSQL(activeFilters: Record) { END AS tool_clean FROM ( SELECT id, unnest(sales_tools) AS tool - FROM responses_2025 + FROM responses_2026 ${whereClause} ) sub ) diff --git a/apps/www/app/state-of-startups/components/charts/WorldOutlookChart.tsx b/apps/www/app/state-of-startups/components/charts/WorldOutlookChart.tsx index e820e355603..5ab29ac84a8 100644 --- a/apps/www/app/state-of-startups/components/charts/WorldOutlookChart.tsx +++ b/apps/www/app/state-of-startups/components/charts/WorldOutlookChart.tsx @@ -1,4 +1,4 @@ -import { SurveyChart, buildWhereClause } from '../SurveyChart' +import { buildWhereClause, SurveyChart } from '../SurveyChart' function generateWorldOutlookSQL(activeFilters: Record) { const whereClause = buildWhereClause(activeFilters) @@ -6,7 +6,7 @@ function generateWorldOutlookSQL(activeFilters: Record) { return `SELECT world_outlook, COUNT(*) AS total -FROM responses_2025${whereClause ? '\n' + whereClause : ''} +FROM responses_2026${whereClause ? '\n' + whereClause : ''} GROUP BY world_outlook ORDER BY total DESC;` } diff --git a/apps/www/app/state-of-startups/components/survey-data-context.tsx b/apps/www/app/state-of-startups/components/survey-data-context.tsx new file mode 100644 index 00000000000..ac291992360 --- /dev/null +++ b/apps/www/app/state-of-startups/components/survey-data-context.tsx @@ -0,0 +1,141 @@ +'use client' + +import { createClient } from '@supabase/supabase-js' +import { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, + type ReactNode, +} from 'react' + +import { statKey, transformSurveyRows } from '../lib/preload-survey-data' +import type { SurveyYear } from './year-context' + +export interface ChartDataItem { + label: string + value: number + rawValue: number +} + +export type StatAggregation = 'single' | 'multi' | 'boolean' + +export interface StatColumnRow { + label: string + count: number +} + +export interface StatColumnData { + rows: StatColumnRow[] + respondentCount: number +} + +export type SurveyDataCache = Record +export type SurveyStatCache = Record + +interface SurveyDataContextValue { + /** Returns cached chart data for an (rpcName, params) combo, or undefined. */ + get: (rpcName: string, params: Record) => ChartDataItem[] | undefined + /** Returns cached stat-column data for a (year, column) pair, or undefined. */ + getStat: (year: SurveyYear, column: string) => StatColumnData | undefined + /** Fetches and caches a non-default chart filter combo. */ + fetchAndCache: (rpcName: string, params: Record) => Promise +} + +const SurveyDataContext = createContext(null) + +const surveyClient = createClient( + process.env.NEXT_PUBLIC_SURVEY_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY! +) + +function paramsKey(params: Record): string { + const keys = Object.keys(params).sort() + if (keys.length === 0) return '' + return JSON.stringify(keys.map((k) => [k, params[k]])) +} + +function cacheKey(rpcName: string, params: Record): string { + return `${rpcName}::${paramsKey(params)}` +} + +export function SurveyDataProvider({ + preloadedData, + preloadedStats, + children, +}: { + preloadedData: SurveyDataCache + preloadedStats: SurveyStatCache + children: ReactNode +}) { + const [cache, setCache] = useState>(() => { + const initial = new Map() + for (const [rpcName, data] of Object.entries(preloadedData)) { + initial.set(cacheKey(rpcName, {}), data) + } + return initial + }) + + const statCache = useMemo(() => { + const map = new Map() + for (const [key, value] of Object.entries(preloadedStats)) { + map.set(key, value) + } + return map + }, [preloadedStats]) + + const inflight = useRef>>(new Map()) + + const get = useCallback( + (rpcName: string, params: Record) => cache.get(cacheKey(rpcName, params)), + [cache] + ) + + const getStat = useCallback( + (year: SurveyYear, column: string) => statCache.get(statKey(year, column)), + [statCache] + ) + + const fetchAndCache = useCallback( + async (rpcName: string, params: Record) => { + const key = cacheKey(rpcName, params) + const cached = cache.get(key) + if (cached) return cached + + const existing = inflight.current.get(key) + if (existing) return existing + + const promise = (async () => { + const { data, error } = await surveyClient.rpc(rpcName, params) + if (error) throw error + const transformed = transformSurveyRows(data ?? []) + setCache((prev) => { + const next = new Map(prev) + next.set(key, transformed) + return next + }) + return transformed + })().finally(() => { + inflight.current.delete(key) + }) + + inflight.current.set(key, promise) + return promise + }, + [cache] + ) + + const value = useMemo(() => ({ get, getStat, fetchAndCache }), [get, getStat, fetchAndCache]) + + return {children} +} + +export function useSurveyDataCache() { + const ctx = useContext(SurveyDataContext) + if (!ctx) { + throw new Error('useSurveyDataCache must be used within a SurveyDataProvider') + } + return ctx +} diff --git a/apps/www/app/state-of-startups/components/year-context.tsx b/apps/www/app/state-of-startups/components/year-context.tsx new file mode 100644 index 00000000000..faed246da7c --- /dev/null +++ b/apps/www/app/state-of-startups/components/year-context.tsx @@ -0,0 +1,37 @@ +'use client' + +import { createContext, useContext, useState, type ReactNode } from 'react' + +export type SurveyYear = 2025 | 2026 + +export const SURVEY_YEARS: readonly SurveyYear[] = [2025, 2026] as const + +interface YearContextValue { + year: SurveyYear + setYear: (year: SurveyYear) => void +} + +const YearContext = createContext(null) + +export function YearProvider({ + children, + defaultYear = 2026, +}: { + children: ReactNode + defaultYear?: SurveyYear +}) { + const [year, setYear] = useState(defaultYear) + return {children} +} + +export function useYear() { + const ctx = useContext(YearContext) + if (!ctx) { + throw new Error('useYear must be used within a YearProvider') + } + return ctx +} + +export function rpcNameForYear(baseName: string, year: SurveyYear) { + return year === 2026 ? `${baseName}_2026` : baseName +} diff --git a/apps/www/app/state-of-startups/lib/preload-survey-data.ts b/apps/www/app/state-of-startups/lib/preload-survey-data.ts new file mode 100644 index 00000000000..55d6f59bac2 --- /dev/null +++ b/apps/www/app/state-of-startups/lib/preload-survey-data.ts @@ -0,0 +1,215 @@ +import { createClient } from '@supabase/supabase-js' + +import type { + ChartDataItem, + StatAggregation, + StatColumnData, + StatColumnRow, +} from '../components/survey-data-context' + +const CARRY_OVER_RPCS = [ + 'get_role_stats', + 'get_location_stats', + 'get_funding_stage_stats', + 'get_accelerator_stats', + 'get_industry_stats', + 'get_biggest_challenge_stats', + 'get_world_outlook_stats', + 'get_databases_stats', + 'get_ai_coding_tools_stats', + 'get_ai_models_stats', + 'get_initial_paying_customers_stats', + 'get_sales_tools_stats', + 'get_new_ideas_stats', + 'get_regular_social_media_use_stats', +] as const + +const NEW_IN_2026_RPCS = [ + 'get_ai_generated_codebase_percent_stats', + 'get_auth_provider_stats', + 'get_subscriptions_stats', + 'get_building_ai_agents_stats', + 'get_mcp_adoption_stats', +] as const + +// Columns referenced by stat cards across the narrative. Preloaded via the +// generic `get__stats` RPCs so the stat cards can compute live +// percentages for both 2025 and 2026 without an extra round trip in the +// browser. Adding a new stat to the narrative? Add its column here too. +const STAT_PRELOAD_QUERIES: Array<{ column: string; aggregation: StatAggregation }> = [ + // single-select + { column: 'founder_count', aggregation: 'single' }, + { column: 'person_age', aggregation: 'single' }, + { column: 'team_size', aggregation: 'single' }, + { column: 'startup_age', aggregation: 'single' }, + { column: 'funding_stage', aggregation: 'single' }, + { column: 'location', aggregation: 'single' }, + { column: 'role', aggregation: 'single' }, + { column: 'industry', aggregation: 'single' }, + { column: 'biggest_challenge', aggregation: 'single' }, + { column: 'world_outlook', aggregation: 'single' }, + { column: 'building_ai_agents', aggregation: 'single' }, + { column: 'ai_generated_codebase_percent', aggregation: 'single' }, + { column: 'mcp_adoption', aggregation: 'single' }, + { column: 'building_multi_agent_systems', aggregation: 'single' }, + { column: 'dev_community_built', aggregation: 'single' }, + { column: 'dedicated_sales_function', aggregation: 'single' }, + { column: 'describe_your_online_persona', aggregation: 'single' }, + // multi-select + { column: 'ai_coding_tools', aggregation: 'multi' }, + { column: 'subscriptions', aggregation: 'multi' }, + { column: 'ai_models_used', aggregation: 'multi' }, + { column: 'databases', aggregation: 'multi' }, + { column: 'backend_stack', aggregation: 'multi' }, + { column: 'frontend_stack', aggregation: 'multi' }, + { column: 'auth_provider', aggregation: 'multi' }, + { column: 'cloud_providers', aggregation: 'multi' }, + { column: 'ai_agents_problems', aggregation: 'multi' }, + { column: 'ai_evaluation_testing', aggregation: 'multi' }, + { column: 'sales_tools', aggregation: 'multi' }, + { column: 'observability', aggregation: 'multi' }, + { column: 'growth_tools', aggregation: 'multi' }, + { column: 'initial_paying_customers', aggregation: 'multi' }, + { column: 'user_engagement', aggregation: 'multi' }, + { column: 'pricing', aggregation: 'multi' }, + { column: 'market_model', aggregation: 'multi' }, + { column: 'regular_social_media_use', aggregation: 'multi' }, + { column: 'events', aggregation: 'multi' }, + // boolean + { column: 'founders_are_technical', aggregation: 'boolean' }, + { column: 'previous_company', aggregation: 'boolean' }, +] + +const STAT_YEARS = [2025, 2026] as const + +export type PreloadedSurveyData = Record +export type PreloadedStatData = Record + +export function transformSurveyRows(rows: any[]): ChartDataItem[] { + if (!Array.isArray(rows) || rows.length === 0) return [] + const total = rows.reduce((sum, row) => sum + parseInt(row.count ?? row.total ?? '0'), 0) + return rows.map((row) => { + const count = parseInt(row.count ?? row.total ?? '0') + const rawPercentage = total > 0 ? (count / total) * 100 : 0 + return { + label: row.label || row.value || row[Object.keys(row)[0]], + value: Math.round(rawPercentage), + rawValue: rawPercentage, + } + }) +} + +function statKey(year: number, column: string): string { + return `${year}::${column}` +} + +export { statKey } + +export async function preloadSurveyData(): Promise { + const url = process.env.NEXT_PUBLIC_SURVEY_SUPABASE_URL + const anonKey = process.env.NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY + + if (!url || !anonKey) { + console.warn('[preloadSurveyData] survey supabase env vars missing — returning empty cache') + return {} + } + + const client = createClient(url, anonKey) + + const rpcNames: string[] = [ + ...CARRY_OVER_RPCS, + ...CARRY_OVER_RPCS.map((name) => `${name}_2026`), + ...NEW_IN_2026_RPCS.map((name) => `${name}_2026`), + ] + + const entries = await Promise.all( + rpcNames.map(async (rpcName) => { + try { + const { data, error } = await client.rpc(rpcName, {}) + if (error) { + console.error(`[preloadSurveyData] ${rpcName} failed:`, error.message) + return [rpcName, [] as ChartDataItem[]] as const + } + return [rpcName, transformSurveyRows(data ?? [])] as const + } catch (err: any) { + console.error(`[preloadSurveyData] ${rpcName} threw:`, err?.message ?? err) + return [rpcName, [] as ChartDataItem[]] as const + } + }) + ) + + return Object.fromEntries(entries) +} + +export async function preloadStatData(): Promise { + const url = process.env.NEXT_PUBLIC_SURVEY_SUPABASE_URL + const anonKey = process.env.NEXT_PUBLIC_SURVEY_SUPABASE_ANON_KEY + + if (!url || !anonKey) { + console.warn('[preloadStatData] survey supabase env vars missing — returning empty cache') + return {} + } + + const client = createClient(url, anonKey) + + const tasks = STAT_PRELOAD_QUERIES.flatMap(({ column, aggregation }) => + STAT_YEARS.map(async (year) => { + const key = statKey(year, column) + try { + const rpcName = + aggregation === 'multi' + ? 'get_multi_select_stats' + : aggregation === 'boolean' + ? 'get_boolean_stats' + : 'get_single_select_stats' + + const { data, error } = await client.rpc(rpcName, { + survey_year: year, + column_name: column, + }) + + if (error) { + // Column doesn't exist for this year (e.g. new-in-2026 column on 2025). + // Silently skip; stat card will fall back to its static value or "—". + return null + } + + const rows: StatColumnRow[] = (data ?? []).map((r: any) => ({ + label: String(r.label), + count: Number(r.count ?? 0), + })) + + let respondentCount: number + if (aggregation === 'multi') { + const { data: rcData, error: rcError } = await client.rpc( + 'get_respondent_count_for_column', + { survey_year: year, column_name: column } + ) + if (rcError) { + console.error( + `[preloadStatData] respondent_count(${year},${column}) failed:`, + rcError.message + ) + return null + } + respondentCount = Number(rcData ?? 0) + } else { + respondentCount = rows.reduce((sum, r) => sum + r.count, 0) + } + + return [key, { rows, respondentCount }] as const + } catch (err: any) { + console.error( + `[preloadStatData] ${aggregation} ${column} ${year} threw:`, + err?.message ?? err + ) + return null + } + }) + ) + + const settled = await Promise.all(tasks) + return Object.fromEntries( + settled.filter((e): e is readonly [string, StatColumnData] => e !== null) + ) +} diff --git a/apps/www/app/state-of-startups/page.tsx b/apps/www/app/state-of-startups/page.tsx index 141d8f5983c..a7c0f0394f3 100644 --- a/apps/www/app/state-of-startups/page.tsx +++ b/apps/www/app/state-of-startups/page.tsx @@ -1,10 +1,16 @@ import type { Metadata } from 'next' +import { preloadStatData, preloadSurveyData } from './lib/preload-survey-data' import { RegisterContent } from './register/RegisterContent' import StateOfStartups2026Content from './StateOfStartups2026Content' // TODO (alan): set this to true once results are available. -const SHOW_RESULTS = false +const SHOW_RESULTS = true + +// Re-aggregate survey RPCs on the server every hour. Keeps the chart payload +// inlined into the static HTML without forcing a full deploy whenever the +// upstream survey data changes. +export const revalidate = 3600 export const metadata: Metadata = SHOW_RESULTS ? { @@ -39,6 +45,14 @@ export const metadata: Metadata = SHOW_RESULTS }, } -export default function StateOfStartupsPage() { - return SHOW_RESULTS ? : +export default async function StateOfStartupsPage() { + if (!SHOW_RESULTS) return + + const [preloadedData, preloadedStats] = await Promise.all([ + preloadSurveyData(), + preloadStatData(), + ]) + return ( + + ) } diff --git a/apps/www/data/surveys/state-of-startups-2026.tsx b/apps/www/data/surveys/state-of-startups-2026.tsx index c53a9eba403..772fc2f15a2 100644 --- a/apps/www/data/surveys/state-of-startups-2026.tsx +++ b/apps/www/data/surveys/state-of-startups-2026.tsx @@ -1,3 +1,10 @@ +// Narrative sourced from the State of Startups 2026 mid-point results +// (supabase/state-of-startups repo, app/fancy/survey-data.ts). Explicit +// percentages below are quoted directly from that narrative; a handful of +// stats for sections without an explicit number in the prose are carried +// over from 2025 and should be refreshed against responses_2026 before +// launch. + const stateOfStartupsData = { metaTitle: 'State of Startups 2026 | Supabase', metaDescription: @@ -7,14 +14,14 @@ const stateOfStartupsData = { title: 'State of Startups 2026', subheader: 'We surveyed over 2,000 startup founders and builders to uncover what’s powering modern startups: their stacks, their go-to-market motion, and their approach to AI.', - cta: 'This report is built for builders.', + cta: 'The ground moved between 2025 and 2026. This report shows where.', }, pageChapters: [ { title: 'Who’s Building Startups', shortTitle: 'Founder and Company', description: - 'Today’s startup ecosystem is dominated by young, technical builders shipping fast with lean teams.', + 'The respondent base got older, more European, more solo, and less self-described-technical. Experienced operators with AI in their pocket are starting companies again.', pullQuote: { quote: 'Our team is just two people at the moment. We’re funding the proof-of-concept stage out of our own pockets.', @@ -24,29 +31,94 @@ const stateOfStartupsData = { }, sections: [ { - title: 'Roles and Experience', + title: 'Solo Founders', description: - 'Founders are overwhelmingly technical and under 40, with most building their first company.', + 'Solo founders are 61% of respondents — the largest group on the page. Cofounder pairs slipped. The one-person startup is the dominant shape and is still growing.', stats: [ - { percent: 81, label: 'Founders that are technical' }, - { percent: 82, label: 'Founders that are under 40' }, - { percent: 36, label: 'Founders that are repeat founders' }, + { + percent: 61, + label: 'One-founder startups', + source: { column: 'founder_count', aggregation: 'single', target: '1' }, + }, + { + percent: 25, + label: 'Two-founder startups', + source: { column: 'founder_count', aggregation: 'single', target: '2' }, + }, + { + percent: 14, + label: 'Three or more founders', + source: { + column: 'founder_count', + aggregation: 'single', + target: ['3', '4+'], + }, + }, ], - charts: ['RoleChart'], + charts: [], wordCloud: undefined, summarizedAnswer: undefined, rankedAnswersPair: undefined, }, { - title: 'Team Size and Funding', + title: 'Non-Technical Founders', description: - 'Startups are mostly bootstrapped or at early stages of funding. They are small teams, and usually less than a year old.', + '78% of founders are technical. The 22% who aren’t lean harder on AI code generation than anyone else.', stats: [ - { percent: 91, label: 'Startups with 10 or fewer employees' }, - { percent: 66, label: 'Startups under one year old' }, - { percent: 6, label: 'Startups over 5 years old' }, + { + percent: 78, + label: 'Startups whose founders are technical', + source: { + column: 'founders_are_technical', + aggregation: 'boolean', + target: 'TRUE', + }, + }, + { + percent: 47, + label: 'Founders who had previously started a company', + source: { + column: 'previous_company', + aggregation: 'boolean', + target: 'TRUE', + }, + }, ], - charts: ['FundingStageChart'], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Older Founders', + description: + 'Every age band above 40 has grown by a statistically significant margin. The 22 to 29 cohort shrank by 5%. Seasoned operators are filling the gap, often with AI doing the typing.', + stats: [ + { + percent: 26, + label: 'Founders aged 40 or older', + source: { + column: 'person_age', + aggregation: 'single', + target: ['40–49', '50–59', '60+'], + }, + }, + { + percent: 35, + label: 'Founders aged 22 to 29', + source: { column: 'person_age', aggregation: 'single', target: '22–29' }, + }, + { + percent: 10, + label: 'Founders aged 50 or older', + source: { + column: 'person_age', + aggregation: 'single', + target: ['50–59', '60+'], + }, + }, + ], + charts: [], wordCloud: undefined, summarizedAnswer: undefined, rankedAnswersPair: undefined, @@ -54,11 +126,23 @@ const stateOfStartupsData = { { title: 'Where They’re Based', description: - 'Startups are building globally, but North America—especially San Francisco—remains overrepresented. Europe and Asia also feature prominently, with hubs like Toronto and NYC following close behind.', + 'The top metros are still the top metros. But AI has flattened the development gap everywhere else. Europe grew 4%; Africa grew 2%. Startups are setting up across Toronto, Chicago, Denver, Austin, across Europe, Asia, and Africa. The playbook no longer requires a specific zip code.', stats: [ - { percent: 25, label: 'Global startups based in Europe' }, - { percent: 19, label: 'North American startups based in San Francisco' }, - { percent: 9, label: 'North American startups based in New York City' }, + { + percent: 25, + label: 'Startups headquartered in Europe', + source: { column: 'location', aggregation: 'single', target: 'Europe' }, + }, + { + percent: 50, + label: 'Startups headquartered in North America', + source: { column: 'location', aggregation: 'single', target: 'North America' }, + }, + { + percent: 14, + label: 'Startups headquartered in Asia', + source: { column: 'location', aggregation: 'single', target: 'Asia' }, + }, ], charts: ['LocationChart'], wordCloud: undefined, @@ -68,121 +152,39 @@ const stateOfStartupsData = { ], }, { - title: 'What Startups are Building', - shortTitle: 'Product and Market', + title: 'One Company Swept the Tooling Layer', + shortTitle: 'Anthropic generation', description: - 'Startups are still experimenting. They’re building a diverse mix of software products, iterating quickly, and pursuing monetization selectively.', - pullQuote: { - quote: - 'We’re building an end-to-end system for wedding planners, all running as one SvelteKit / Supabase instance.', - author: 'Waldemar Pross', - authorPosition: 'CTO, Peach Perfect Weddings', - authorAvatar: '/images/state-of-startups/quote-avatars/waldemar-k-120x120.jpg', - }, + 'Claude Code became the most-named must-have dev tool. Claude paid subscriptions nearly doubled. Anthropic overtook OpenAI on the model-provider question. The Anthropic Agent SDK leads SDK adoption.', + pullQuote: undefined, sections: [ { - title: 'Industries and Focus', + title: 'Must-have Dev Tools', description: - 'Under-30s gravitate toward AI-driven productivity, education, and social tools; areas where rapid iteration and novelty matter. Over-50s skew toward SaaS and consumer products, often bringing domain-specific experience into more established markets. Developer tools and infrastructure attract all age groups.', + 'Cursor dropped 19%. v0, Bolt, and Windsurf each lost 6–9%. VS Code held flat. Antigravity appeared for the first time and took the 4th spot in its debut year.', stats: [ - { percent: 82, label: 'Founders under 30 building in AI/ML' }, - { percent: 60, label: 'Startups building for end consumers' }, - { percent: 16, label: 'Startups building for developers' }, - ], - charts: ['IndustryChart'], - - wordCloud: undefined, - summarizedAnswer: { - label: 'Problems startups are solving', - answers: [ - 'AI-powered productivity tools', - 'Agent workflows (internal or customer-facing)', - 'Career preparation and job search', - 'AI copilots for small businesses', - 'AI-enhanced education and tutoring', - 'Tools for solopreneurs and creators', - 'Developer experience and API abstraction', - 'Sales and outreach automation', - 'Healthcare access and diagnostics', - 'Financial planning and forecasting', - 'Sustainability and climate data', - 'Privacy and compliance automation', - 'Time management and prioritization', - 'Collaboration and communication', - 'Mental health and wellness tracking', - ], - }, - rankedAnswersPair: undefined, - }, - { - title: 'Traction and Early Growth', - description: - 'One in five startups joined an accelerator. Y Combinator is the most common choice, especially in North America. Elsewhere, participation was more evenly distributed. Pivoting remains the norm, and less than half of startups are monetizing today.', - stats: [ - { percent: 64, label: 'Startups that are pre-revenue' }, - { percent: 59, label: 'Startups that pivoted at least once' }, - { percent: 19, label: 'Startups that joined accelerators' }, - ], - charts: ['AcceleratorParticipationChart'], - wordCloud: undefined, - summarizedAnswer: undefined, - rankedAnswersPair: undefined, - }, - ], - }, - { - title: 'What’s in a Startup’s Tech Stack', - shortTitle: 'Tech Stack', - description: - 'The modern stack centers around open tools, modular infrastructure, and cautious spending.', - pullQuote: { - quote: - 'Cursor has been my favourite tool so far. It’s made my life easier by documenting code on my behalf.', - author: 'Kevinton B', - authorPosition: 'Engineer, FlutterFlow', - authorAvatar: '/images/state-of-startups/quote-avatars/kevinton-b-120x120.jpg', - }, - sections: [ - { - title: 'Frameworks and Cloud Infra', - description: - 'Supabase and Postgres dominate backend infrastructure. React and Node top frontend and backend respectively. Cursor, Claude, and VS Code lead AI-assisted development. Developer tools like GitHub, Stripe, and Postman round out the stack.', - stats: [ - { - percent: 83, - label: 'Startups with a JavaScript framework in their frontend stack', - }, { percent: 62, - label: 'Startups with Supabase in their cloud provider stack', + label: 'Startups that list Claude Code as a must-have', + source: { column: 'ai_coding_tools', aggregation: 'multi', target: 'Claude Code' }, + }, + { + percent: 37, + label: 'Startups that list Cursor as a must-have', + source: { column: 'ai_coding_tools', aggregation: 'multi', target: 'Cursor' }, }, - { percent: 60, label: 'Startups with Node.js in their backend stack' }, - ], - charts: ['DatabasesChart'], - - wordCloud: undefined, - summarizedAnswer: undefined, - rankedAnswersPair: undefined, - }, - { - title: 'Dev Tools and Time Savers', - description: - 'AI coding tools are indispensable for startups, and not just Cursor and Visual Studio Code. ‘Vibe coding’ tools like Loveable, Bolt.new, and v0 are also common.', - stats: [ - { percent: 57, label: 'Startups that pay for OpenAI or ChatGPT' }, - { percent: 37, label: 'Startups that pay for Cursor' }, { percent: 12, label: 'Startups that don’t pay for AI tools at all' }, ], charts: ['AICodingToolsChart'], wordCloud: { label: 'Must-have developer tools by keyword frequency', words: [ - { text: 'cursor', count: 495 }, - { text: 'code', count: 396 }, + { text: 'claude', count: 512 }, + { text: 'code', count: 420 }, + { text: 'cursor', count: 315 }, { text: 'supabase', count: 302 }, { text: 'github', count: 272 }, { text: 'vscode', count: 160 }, - { text: 'claude', count: 143 }, { text: 'docker', count: 114 }, { text: 'git', count: 113 }, { text: 'studio', count: 112 }, @@ -198,6 +200,7 @@ const stateOfStartupsData = { { text: 'windsurf', count: 51 }, { text: 'backend', count: 44 }, { text: 'api', count: 40 }, + { text: 'gemini', count: 39 }, { text: 'google', count: 37 }, { text: 'testing', count: 36 }, { text: 'tailwind', count: 35 }, @@ -205,27 +208,136 @@ const stateOfStartupsData = { { text: 'typescript', count: 33 }, { text: 'control', count: 33 }, { text: 'python', count: 32 }, - { text: 'gemini', count: 30 }, + { text: 'antigravity', count: 28 }, ], }, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Paid AI Subscriptions', + description: + 'Paid Claude subscriptions sit at 59% of respondents. Paid OpenAI is at 39%. Gemini entered the list at 27%.', + stats: [ + { + percent: 59, + label: 'Startups that pay for Claude', + source: { column: 'subscriptions', aggregation: 'multi', target: 'Claude' }, + }, + { + percent: 39, + label: 'Startups that pay for OpenAI or ChatGPT', + source: { + column: 'subscriptions', + aggregation: 'multi', + target: 'OpenAI / ChatGPT', + }, + }, + { + percent: 27, + label: 'Startups that pay for Gemini', + source: { column: 'subscriptions', aggregation: 'multi', target: 'Gemini' }, + }, + ], + charts: ['PaidSubscriptionsChart'], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Model Providers', + description: + 'Anthropic/Claude climbed from 38% to 64%; OpenAI fell from 69% to 51%. Gemini entered at 43%. Hugging Face and custom models lost material share, suggesting fewer teams are running their own inference.', + stats: [ + { + percent: 64, + label: 'Startups using Anthropic models', + source: { + column: 'ai_models_used', + aggregation: 'multi', + target: 'Anthropic/Claude', + }, + }, + { + percent: 51, + label: 'Startups using OpenAI models', + source: { column: 'ai_models_used', aggregation: 'multi', target: 'OpenAI' }, + }, + { + percent: 43, + label: 'Startups using Gemini', + source: { column: 'ai_models_used', aggregation: 'multi', target: 'Gemini' }, + }, + ], + charts: ['AIModelsChart'], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + ], + }, + { + title: 'AI-generated Code is the Median Experience', + shortTitle: 'AI-written code', + description: + '62% of startups have more than half their codebase written by AI. 41% are at 76–100%. Only 2% are at zero. Older founders use it more heavily than younger ones, and non-technical founders more than technical ones.', + pullQuote: { + quote: + 'Cursor has been my favourite tool so far. It’s made my life easier by documenting code on my behalf.', + author: 'Kevinton B', + authorPosition: 'Engineer, FlutterFlow', + authorAvatar: '/images/state-of-startups/quote-avatars/kevinton-b-120x120.jpg', + }, + sections: [ + { + title: 'Share of Codebase', + newInYear: 2026, + description: + 'Among non-technical founders, the 76-to-100% share rises to 54%. Among 50-to-59-year-olds it rises to 80%. AI code generation is a median practice, not a fringe one.', + stats: [ + { + percent: 41, + label: 'Startups with 76–100% of their codebase AI-generated', + source: { + column: 'ai_generated_codebase_percent', + aggregation: 'single', + target: '76-100%', + }, + }, + { + percent: 62, + label: 'Startups with a majority AI-generated codebase', + source: { + column: 'ai_generated_codebase_percent', + aggregation: 'single', + target: ['51-75%', '76-100%'], + }, + }, + { + percent: 2, + label: 'Startups with zero AI-generated code', + source: { + column: 'ai_generated_codebase_percent', + aggregation: 'single', + target: '0%', + }, + }, + ], + charts: ['AICodebasePercentChart'], + wordCloud: undefined, summarizedAnswer: { - label: 'Tools that startups wish existed', + label: 'How AI is changing the way startups build', answers: [ - 'Unified backend platform combining auth, edge, database, and queues', - 'AI agents with real memory and workflow context', - 'Local-first dev environments that sync to Supabase or Git', - 'AI copilots for sales, marketing, or documentation', - 'UI builders with direct-to-code export and stateful logic', - 'Better CLI-driven or REPL-native dev tools', - 'Automated integration layers between SaaS APIs', - 'Real-time dashboards that don’t require BI tools', - 'Supabase + Neon or PlanetScale seamless sync', - 'One-click staging, testing, and preview environments', - 'AI validators for production database migrations', - 'Visual version control and state inspection for app logic', - 'Time-aware tools (versioned environments, snapshots, undoable infra)', - 'GPT-based toolchain composers (meta-dev agents)', - 'Agent-like task runners for cron jobs, workflows, monitoring', + 'Menial coding tasks handed off entirely', + 'Founders focused on design and architecture', + 'Non-technical founders shipping production code', + 'Docs and tests written alongside features', + 'Faster iteration on product-market fit', + 'Smaller teams doing the work of larger ones', + 'Prototypes in hours instead of weeks', + 'Refactors and migrations that used to be deferred', + 'More confidence picking up unfamiliar stacks', + 'Solo founders competing with funded teams', ], }, rankedAnswersPair: undefined, @@ -233,10 +345,126 @@ const stateOfStartupsData = { ], }, { - title: 'How Startups are Integrating AI', + title: 'The Stack Consolidated', + shortTitle: 'Tech Stack', + description: + 'Supabase gained ground as a primary database, and combined with Postgres, it’s clear what platform startups are betting on. Hyperscalers lost share. And the frontend layer is diversifying fast.', + pullQuote: { + quote: + 'We’re building an end-to-end system for wedding planners, all running as one SvelteKit / Supabase instance.', + author: 'Waldemar Pross', + authorPosition: 'CTO, Peach Perfect Weddings', + authorAvatar: '/images/state-of-startups/quote-avatars/waldemar-k-120x120.jpg', + }, + sections: [ + { + title: 'Primary Database', + description: + 'Supabase went from 76% to 82%. Every legacy NoSQL option lost share: MongoDB dropped 5%, MySQL 3%, Firebase 2%. Neon, DynamoDB, and Convex appeared as options for the first time and took small but measurable shares.', + stats: [ + { + percent: 82, + label: 'Startups using Supabase as a database', + source: { column: 'databases', aggregation: 'multi', target: 'Supabase' }, + }, + { percent: 60, label: 'Startups with Node.js in their backend stack' }, + { percent: 83, label: 'Startups with a JavaScript framework in their frontend stack' }, + ], + charts: ['DatabasesChart'], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Auth and Identity', + newInYear: 2026, + description: + 'Three in four respondents who answered the auth question picked Supabase Auth.', + stats: [ + { + percent: 72, + label: 'Startups using Supabase Auth', + source: { + column: 'auth_provider', + aggregation: 'multi', + target: 'Supabase Auth', + }, + }, + { + percent: 15, + label: 'Startups using Auth0', + source: { column: 'auth_provider', aggregation: 'multi', target: 'Auth0' }, + }, + { + percent: 14, + label: 'Startups using NextAuth or Auth.js', + source: { + column: 'auth_provider', + aggregation: 'multi', + target: 'NextAuth / Auth.js', + }, + }, + ], + charts: ['AuthProviderChart'], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Hosting and Cloud', + description: + 'Supabase held its lead. Vercel extended its lead over AWS by 9%. Cloudflare grew fastest of all, crossing 27% and passing AWS on the way up. Every hyperscaler lost share.', + stats: [ + { + percent: 50, + label: 'Startups hosting on Supabase', + source: { column: 'cloud_providers', aggregation: 'multi', target: 'Supabase' }, + }, + { + percent: 47, + label: 'Startups hosting on Vercel', + source: { column: 'cloud_providers', aggregation: 'multi', target: 'Vercel' }, + }, + { + percent: 27, + label: 'Startups hosting on Cloudflare', + source: { column: 'cloud_providers', aggregation: 'multi', target: 'Cloudflare' }, + }, + ], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Frontend Diversifies', + description: + 'React and Next.js both grew. But the quieter story is that four tools went from effectively zero to real share in 12 months: Expo 10%, TanStack 8%, HTMX 4%, Astro 3%. Native mobile also picked up 3 points.', + stats: [ + { + percent: 10, + label: 'Startups using Expo', + source: { column: 'frontend_stack', aggregation: 'multi', target: 'Expo' }, + }, + { + percent: 8, + label: 'Startups using TanStack', + source: { column: 'frontend_stack', aggregation: 'multi', target: 'TanStack' }, + }, + { percent: 3, label: 'Increase in native iOS / Android adoption' }, + ], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + ], + }, + { + title: 'Agents Shipped. The Operator Tools Did Not.', shortTitle: 'AI and Agents', description: - 'AI is a core product capability, not an afterthought. Most teams are using models like OpenAI or Claude for real features, not just demos.', + 'Half of respondents are building agents. Multi-agent systems are in production at a quarter of them. MCP adoption crossed 57% in its first year. But the operational layer beneath all of this is missing: most teams do not monitor AI workloads, most have no formal prompt management, and one in three has no eval process.', pullQuote: { quote: 'AI is embedded in how we build and scale. From using Claude and Cursor in dev, to voice AI in product for smarter, faster recruiting (which is our business).', @@ -246,30 +474,211 @@ const stateOfStartupsData = { }, sections: [ { - title: 'In-Product AI Use', + title: 'Who’s Building Agents', description: - 'Most startups are already integrating models like OpenAI or Claude, especially for semantic search, summarisation, and customer support. Half are building agents to automate real tasks, from onboarding flows to sales triage.', + 'Agent-building share is statistically flat year over year. The “not sure” cohort shrank, which means undecided builders are making up their minds and shipping. What they automate has shifted: workflow and data analysis climbed; customer support (chat bots) fell off the top spot.', stats: [ { - percent: 81, - label: 'Startups using AI in their product', + percent: 52, + label: 'Startups building or planning to build AI agents', + source: { column: 'building_ai_agents', aggregation: 'single', target: 'Yes' }, }, - { percent: 50, label: 'Startups building agents within their product' }, + { percent: 34, label: 'Startups with agents automating customer support' }, { - percent: 34, - label: 'Startups with agents automating customer support', + percent: 16, + label: 'Startups that are not building AI agents', + source: { column: 'building_ai_agents', aggregation: 'single', target: 'No' }, }, ], - charts: ['AIModelsChart'], + charts: ['BuildingAgentsChart'], wordCloud: undefined, summarizedAnswer: { - label: 'Most important AI use cases in product', + label: 'Most common AI agent use cases', answers: [ - 'Summarization / content generation', - 'Recommendations / personalization', - 'Workflow / agent-based automation', - 'Search / semantic search', - 'Customer support automation', + 'Workflow and process automation', + 'Data analysis and reporting', + 'Customer support triage', + 'Sales outreach and lead qualification', + 'Onboarding and activation flows', + 'Content generation and summarization', + 'Internal knowledge search', + 'Developer productivity and code review', + 'Scheduling and meeting assistance', + 'Personalization and recommendations', + ], + }, + rankedAnswersPair: undefined, + }, + { + title: 'Multi-agent Systems', + newInYear: 2026, + description: + 'Three in four agent-builders are already doing multi-agent work. 25% say it is in production. 21% in development. 36% are planning. Only 16% said no.', + stats: [ + { + percent: 25, + label: 'Agent builders running multi-agent systems in production', + source: { + column: 'building_multi_agent_systems', + aggregation: 'single', + target: 'Yes, in production', + }, + }, + { + percent: 21, + label: 'Agent builders with multi-agent systems in development', + source: { + column: 'building_multi_agent_systems', + aggregation: 'single', + target: 'Yes, in development', + }, + }, + { + percent: 36, + label: 'Agent builders planning multi-agent systems', + source: { + column: 'building_multi_agent_systems', + aggregation: 'single', + target: 'Planning to', + }, + }, + ], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'The Operational Gap', + newInYear: 2026, + description: + 'Prompt management, evaluation, and AI observability all paint the same picture: most teams ship agents without the operator stack underneath them.', + stats: [], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'MCP Adoption', + newInYear: 2026, + description: + 'A year after the protocol launched, 57% of respondents have already adopted it. Only 14% say they are unfamiliar.', + stats: [ + { percent: 57, label: 'Respondents with hands on MCP servers or tools' }, + { percent: 14, label: 'Respondents unfamiliar with MCP' }, + { percent: 29, label: 'Respondents still evaluating MCP' }, + ], + charts: ['MCPAdoptionChart'], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + ], + }, + { + title: 'Every Operator Tool Lost Ground', + shortTitle: 'Operator tools', + description: + 'Five separate questions show the same pattern this year. CRMs, analytics, observability, dev communities, prompt management. In every one, named vendors lost share, and either “we don’t have one yet” or “custom-built” grew. Startups are not buying operator tools. They are building their own or doing without.', + pullQuote: undefined, + sections: [ + { + title: 'Sales Tools and CRM Absence', + description: + '53% of startups with a GTM motion have no formal CRM, up from 43%. Every named CRM lost share: HubSpot, Salesforce, Notion/Airtable, Google Sheets. Build-your-own, or nothing at all.', + stats: [ + { + percent: 53, + label: 'Startups with no formal CRM', + source: { + column: 'sales_tools', + aggregation: 'multi', + target: 'We don’t have a formal CRM or sales tool yet', + }, + }, + { + percent: 18, + label: 'Startups using Google Sheets as a CRM', + source: { column: 'sales_tools', aggregation: 'multi', target: 'Google Sheets' }, + }, + { + percent: 12, + label: 'Startups using HubSpot', + source: { column: 'sales_tools', aggregation: 'multi', target: 'HubSpot' }, + }, + ], + charts: ['SalesToolsChart'], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Observability', + description: + '56% still don’t use observability tools. “Custom solution” is the fastest-growing answer, up 2.5%. Datadog and Prometheus both lost share. Sentry kept its lead and grew slightly. The observability market is bifurcating between Sentry for errors and custom dashboards for everything else.', + stats: [ + { + percent: 56, + label: 'Startups that don’t use observability tools', + source: { + column: 'observability', + aggregation: 'multi', + target: 'We don’t use observability tools yet', + }, + }, + ], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Analytics and Growth Tools', + description: + '“I don’t track this yet” grew 5%. HubSpot, Salesforce, Mixpanel, Segment all lost share. Custom-built dashboards grew 6%. The pattern repeats: skip the vendor, ship something internal, or don’t track at all.', + stats: [], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Developer Communities', + description: + '“No, we haven’t built a community” grew 4 points to 48%. “In progress / planning to” shrank. The “yes, we built one” share is flat at 11%. Dev-community-led marketing has a smaller top-of-funnel this year than last.', + stats: [ + { + percent: 48, + label: 'Startups that have not built a developer community', + source: { column: 'dev_community_built', aggregation: 'single', target: 'No' }, + }, + { + percent: 11, + label: 'Startups that have built a developer community', + source: { column: 'dev_community_built', aggregation: 'single', target: 'Yes' }, + }, + ], + charts: [], + wordCloud: undefined, + summarizedAnswer: { + label: 'Operator tools startups wish existed', + answers: [ + 'Unified backend platform combining auth, edge, database, and queues', + 'AI agents with real memory and workflow context', + 'Local-first dev environments that sync to Supabase or Git', + 'AI copilots for sales, marketing, or documentation', + 'UI builders with direct-to-code export and stateful logic', + 'Better CLI-driven or REPL-native dev tools', + 'Automated integration layers between SaaS APIs', + 'Real-time dashboards that don’t require BI tools', + 'One-click staging, testing, and preview environments', + 'AI validators for production database migrations', + 'Visual version control and state inspection for app logic', + 'Agent-like task runners for cron jobs, workflows, monitoring', + 'Prompt versioning and evaluation as a first-class product', + 'Observability that understands AI workloads natively', + 'CRMs designed for founder-led, product-led motions', ], }, rankedAnswersPair: undefined, @@ -277,38 +686,102 @@ const stateOfStartupsData = { ], }, { - title: 'Where Startups Go to Learn', - shortTitle: 'Influence', - description: 'Online communities are the learning engine behind every early-stage startup.', + title: 'Founder-led. No CRM. Mostly Bootstrapped.', + shortTitle: 'Go-To-Market', + description: + 'Founders still do sales themselves. Two in three have never tried paid acquisition. Pricing is settling on tiered feature plans for the first time.', pullQuote: undefined, sections: [ { - title: 'Online Communities', + title: 'Initial Customers', description: - 'There is a healthy diaspora of important online communities. That said, many people just lurk; few actively contribute to the discussion.', + 'Personal networks remain the top source of initial paying customers. 67% of respondents still haven’t tried paid acquisition at all, up from 62% last year.', stats: [ - { percent: 55, label: 'Engineers using LinkedIn regularly' }, - { percent: 45, label: 'Founders using X (Twitter) regularly' }, - { percent: 7, label: 'Respondents that don’t use social media at all' }, + { + percent: 67, + label: + 'Startups that get their first customers via personal and professional networks', + source: { + column: 'initial_paying_customers', + aggregation: 'multi', + target: 'Personal/professional network', + }, + }, + { percent: 48, label: 'Startups that engage users through social media' }, + { percent: 36, label: 'Startups that use tiered feature plans for pricing' }, ], - charts: ['RegularSocialMediaUseChart'], + charts: ['InitialPayingCustomersChart'], wordCloud: undefined, summarizedAnswer: undefined, rankedAnswersPair: undefined, }, { - title: 'Inspiration Stack', + title: 'Sales Motion', description: - 'Founders follow newsletters like TLDR and Lenny’s, and they listen to podcasts like The Diary of a CEO and Founders. Tool discovery happens quite often via YouTube or GitHub. Physical event participation remains low.', + 'Dedicated full-time sales hires usually do not arrive until after the tenth employee. Product-led growth as a motion climbed 4 points to half of respondents. “Not sure yet” is shrinking.', stats: [ - { percent: 47, label: 'Respondents that listen to industry podcasts' }, { - percent: 20, - label: 'Respondents that subscribe to industry newsletters', + percent: 50, + label: 'Startups using a product-led growth motion', + source: { + column: 'market_model', + aggregation: 'multi', + target: 'Product-led growth', + }, }, - { percent: 12, label: 'Founders that have built a developer community' }, ], - charts: ['NewIdeasChart'], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + { + title: 'Pricing Settles', + description: + 'For the first time in the survey, startups are picking a pricing shape earlier in their lifecycle, and they are picking the same one. Tiered feature plans went from 23% to 36% of respondents. The “still experimenting” cohort shrank.', + stats: [ + { + percent: 36, + label: 'Startups using tiered feature plans', + source: { + column: 'pricing', + aggregation: 'multi', + target: 'Tiered feature plans', + }, + }, + ], + charts: [], + wordCloud: undefined, + summarizedAnswer: undefined, + rankedAnswersPair: undefined, + }, + ], + }, + { + title: 'Founders Are Broadcasting Less', + shortTitle: 'Where they show up', + description: + 'Conferences emptied out. Social media lost users across every major platform except TikTok. 1 in 10 respondents now says they have given up on social media entirely. 1 in 3 says they have no online persona at all.', + pullQuote: undefined, + sections: [ + { + title: 'The Quiet Exit From Social', + description: + 'X lost 6 points. LinkedIn lost 3. Reddit and Discord lost 3–4. TikTok was the only platform that grew. The “I have no online persona” share grew 5 points to 33%. One in three respondents is fully offline.', + stats: [ + { + percent: 10, + label: 'Founders that have given up on social media', + source: { + column: 'regular_social_media_use', + aggregation: 'multi', + target: 'I’ve given up social media', + }, + }, + { percent: 33, label: 'Respondents with no online persona' }, + { percent: 55, label: 'Engineers using LinkedIn regularly' }, + ], + charts: ['RegularSocialMediaUseChart'], wordCloud: undefined, summarizedAnswer: undefined, rankedAnswersPair: [ @@ -322,49 +795,22 @@ const stateOfStartupsData = { }, ], }, - ], - }, - { - title: 'How Startups are Finding Customers', - shortTitle: 'Go-To-Market', - description: - 'Startups start selling through their networks and dev communities. Only when they grow do they layer in more structured growth via CRMs and sales.', - pullQuote: undefined, - sections: [ { - title: 'Initial Customers', + title: 'Conferences Fell Off', description: - 'Founders earn their earliest customers through networks, communities, and inbound content. Paid acquisition rarely works early on, nor does performance marketing.', + '2 in 3 respondents are not attending any industry conference. The “none of the above” cohort jumped 10 points. Google Cloud Next, AWS re:Invent, Microsoft Build, and Y Combinator Demo Day all lost share. Conference-led developer marketing is working for a smaller slice of the market every year.', stats: [ { - percent: 58, - label: - 'Startups that get their first customers via personal and professional networks', + percent: 67, + label: 'Respondents not attending any industry conference', + source: { + column: 'events', + aggregation: 'multi', + target: 'None of the above', + }, }, - { percent: 48, label: 'Startups that engage users through social media' }, - { percent: 39, label: 'Startups that are still experimenting with pricing models' }, ], - charts: ['InitialPayingCustomersChart'], - wordCloud: undefined, - summarizedAnswer: undefined, - rankedAnswersPair: undefined, - }, - { - title: 'Founder-led Sales', - description: - 'Sales is still founder-led at most startups. Dedicated sales hires usually don’t arrive until after 10+ employees. Many still use Google Sheets or nothing at all to track sales activity.', - stats: [ - { - percent: 75, - label: 'Startups with their founders still directly responsible for sales', - }, - { - percent: 58, - label: 'Startups that got their initial customers from personal networks', - }, - { percent: 7, label: 'Startups that have a dedicated sales team' }, - ], - charts: ['SalesToolsChart'], + charts: [], wordCloud: undefined, summarizedAnswer: undefined, rankedAnswersPair: undefined, @@ -372,10 +818,10 @@ const stateOfStartupsData = { ], }, { - title: 'Biggest Challenges for Startups', + title: 'Technical Complexity Collapsed. New Fears Took Its Place.', shortTitle: 'Outlook', description: - 'Startups remain optimistic about the future but are weighed down by technical complexity, customer acquisition hurdles, and a wish list of tools that still don’t exist.', + 'The biggest single movement in the entire survey: “technical complexity” as the largest business challenge fell from 24% to 11%. AI ate the hard parts of shipping. What replaced it: burn out, AI-competition fear, runway anxiety. Optimism is mostly flat. Engineers less so.', pullQuote: { quote: 'There’s plenty of uncertainty, but we’re building something that feels deeply worth it. That gives us a lot of confidence in the long run.', @@ -387,24 +833,37 @@ const stateOfStartupsData = { { title: 'The Road Ahead', description: - 'The hardest problems are still the oldest ones: customer acquisition, product-market fit, and complexity. Startups cite AI-assisted coding and backend services as major time-savers, but many are still missing critical tools they want. Especially around onboarding, dashboards, and agents.', + 'The largest year-over-year shift in any single category. Three new challenge options came online: burn out, AI competition, runway anxiety. Together they absorb roughly the same share that used to pick technical complexity. Among 1–10 person teams, burn out has already overtaken technical complexity as the second-biggest challenge.', stats: [ { - percent: 82, - label: 'Founders that evaluate tools via hands-on experience', + percent: 30, + label: 'Startups whose biggest challenge is customer acquisition', + source: { + column: 'biggest_challenge', + aggregation: 'single', + target: 'Customer acquisition', + }, }, { - percent: 45, - label: - 'Startups with over 250 employees whose biggest challenge is getting customers', + percent: 20, + label: 'Startups whose biggest challenge is product-market fit', + source: { + column: 'biggest_challenge', + aggregation: 'single', + target: 'Product-market fit', + }, }, { - percent: 4, - label: 'Startups with under 10 employees whose biggest challenge is hiring', + percent: 11, + label: 'Startups whose biggest challenge is technical complexity', + source: { + column: 'biggest_challenge', + aggregation: 'single', + target: 'Technical complexity', + }, }, ], charts: ['BiggestChallengeChart'], - wordCloud: undefined, summarizedAnswer: undefined, rankedAnswersPair: undefined, @@ -412,12 +871,11 @@ const stateOfStartupsData = { { title: 'Worldview and Optimism', description: - 'Most startup founders remain upbeat about the future, but that confidence isn’t shared equally. Engineers and marketers show more caution.', - + '56% say they are optimistic, down 2 points from last year, not statistically significant. Founders are 58% optimistic; non-founders are 49%. The gap widens among engineers and marketers.', stats: [ - { percent: 61, label: 'Founders that are optimistic' }, - { percent: 50, label: 'Engineers that are optimistic' }, - { percent: 42, label: 'Other roles that are optimistic' }, + { percent: 58, label: 'Founders that are optimistic' }, + { percent: 49, label: 'Non-founders that are optimistic' }, + { percent: 56, label: 'Respondents overall that are optimistic' }, ], charts: ['WorldOutlookChart'], wordCloud: undefined, @@ -448,7 +906,6 @@ const stateOfStartupsData = { company: 'Shor', url: 'https://tryshor.com', }, - { company: 'Mono', url: 'https://www.mono.la',