From 4617b6f4cbee49eb181cc7bb0dda74706b25b009 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Fri, 2 Oct 2026 16:14:13 +0200 Subject: [PATCH] feat: compute landing page (#50899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compute landing page. ## Summary by CodeRabbit * **New Features** * Added a Compute product page introducing ephemeral sandboxes and always-on HTTP services, with information on workloads, security, deployment, and common questions. * Added a private-alpha waitlist form with submission confirmation and error feedback. * Added Compute links and a “Private Alpha” badge in product navigation and the footer. * Added an animated diagram illustrating Compute workloads and their lifecycle. * **Documentation** * Added a Compute overview covering runtimes, scaling, security, deployment, and availability. --- .../compute/_components/CTASection.tsx | 24 + .../compute/_components/ComputeContent.tsx | 37 + .../compute/_components/ComputeLogo.tsx | 42 + .../compute/_components/ComputeVisual.tsx | 716 ++++++++++++++++++ .../compute/_components/DeploySection.tsx | 91 +++ .../compute/_components/FAQSection.tsx | 70 ++ .../(products)/compute/_components/Hero.tsx | 33 + .../compute/_components/Highlights.tsx | 41 + .../_components/TrustBoundarySection.tsx | 70 ++ .../compute/_components/WaitlistForm.tsx | 263 +++++++ .../compute/_components/WorkloadsSection.tsx | 105 +++ .../_components/compute-visual.module.css | 19 + apps/www/app/(products)/compute/page.tsx | 15 + apps/www/app/(products)/layout.tsx | 1 + .../submit-form-compute-waitlist/route.tsx | 178 +++++ apps/www/components/Enterprise/Support.tsx | 34 +- apps/www/components/FeatureItem.tsx | 29 + apps/www/components/Nav/MenuItem.tsx | 8 +- apps/www/components/Nav/MobileMenu.tsx | 5 + apps/www/components/Nav/ProductDropdown.tsx | 5 + apps/www/content/md/compute.md | 44 ++ apps/www/data/Footer.ts | 4 + apps/www/data/MainProducts.tsx | 13 + apps/www/turbo.jsonc | 1 + packages/shared-data/products.ts | 11 + 25 files changed, 1826 insertions(+), 33 deletions(-) create mode 100644 apps/www/app/(products)/compute/_components/CTASection.tsx create mode 100644 apps/www/app/(products)/compute/_components/ComputeContent.tsx create mode 100644 apps/www/app/(products)/compute/_components/ComputeLogo.tsx create mode 100644 apps/www/app/(products)/compute/_components/ComputeVisual.tsx create mode 100644 apps/www/app/(products)/compute/_components/DeploySection.tsx create mode 100644 apps/www/app/(products)/compute/_components/FAQSection.tsx create mode 100644 apps/www/app/(products)/compute/_components/Hero.tsx create mode 100644 apps/www/app/(products)/compute/_components/Highlights.tsx create mode 100644 apps/www/app/(products)/compute/_components/TrustBoundarySection.tsx create mode 100644 apps/www/app/(products)/compute/_components/WaitlistForm.tsx create mode 100644 apps/www/app/(products)/compute/_components/WorkloadsSection.tsx create mode 100644 apps/www/app/(products)/compute/_components/compute-visual.module.css create mode 100644 apps/www/app/(products)/compute/page.tsx create mode 100644 apps/www/app/api-v2/submit-form-compute-waitlist/route.tsx create mode 100644 apps/www/components/FeatureItem.tsx create mode 100644 apps/www/content/md/compute.md diff --git a/apps/www/app/(products)/compute/_components/CTASection.tsx b/apps/www/app/(products)/compute/_components/CTASection.tsx new file mode 100644 index 00000000000..b4d362651cb --- /dev/null +++ b/apps/www/app/(products)/compute/_components/CTASection.tsx @@ -0,0 +1,24 @@ +import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn' +import Link from 'next/link' + +import { WaitlistForm } from './WaitlistForm' + +export function CTASection() { + return ( + +
+
+

+ Run your next workload + next to your data +

+

+ Compute is in Private Alpha. Join the waitlist and we'll reach out as invites roll + out. +

+
+ +
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/ComputeContent.tsx b/apps/www/app/(products)/compute/_components/ComputeContent.tsx new file mode 100644 index 00000000000..cb92405d5fc --- /dev/null +++ b/apps/www/app/(products)/compute/_components/ComputeContent.tsx @@ -0,0 +1,37 @@ +import { ComputeVisual } from './ComputeVisual' +import { CTASection } from './CTASection' +import { DeploySection } from './DeploySection' +import { FAQSection } from './FAQSection' +import { Hero } from './Hero' +import { Highlights } from './Highlights' +import { TrustBoundarySection } from './TrustBoundarySection' +import { WorkloadsSection } from './WorkloadsSection' + +export function ComputeContent() { + return ( +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/ComputeLogo.tsx b/apps/www/app/(products)/compute/_components/ComputeLogo.tsx new file mode 100644 index 00000000000..86b4109ecc1 --- /dev/null +++ b/apps/www/app/(products)/compute/_components/ComputeLogo.tsx @@ -0,0 +1,42 @@ +interface ComputeLogoProps { + size?: number + className?: string +} + +export function ComputeLogo({ size = 18, className }: ComputeLogoProps) { + return ( + + + + + + + ) +} diff --git a/apps/www/app/(products)/compute/_components/ComputeVisual.tsx b/apps/www/app/(products)/compute/_components/ComputeVisual.tsx new file mode 100644 index 00000000000..c9520572ef0 --- /dev/null +++ b/apps/www/app/(products)/compute/_components/ComputeVisual.tsx @@ -0,0 +1,716 @@ +'use client' + +import { AnimatePresence, motion, useReducedMotion } from 'framer-motion' +import { Database } from 'icons' +import type { LucideIcon } from 'lucide-react' +import { Bot, User } from 'lucide-react' +import { useCallback, useEffect, useId, useRef, useState } from 'react' +import { cn } from 'ui' + +import SectionContainerWithCn from '../../../../components/Layouts/SectionContainerWithCn' +import styles from './compute-visual.module.css' +import { ComputeLogo } from './ComputeLogo' + +// Ephemeral sandboxes cycle through this lifecycle on a loop: empty (no +// container running) -> deploying -> active -> stopping -> suspending -> empty. +// Durations are compressed relative to a real sandbox's lifetime ("a few +// minutes" of activity becomes tens of seconds, etc.) so the loop stays +// watchable on a marketing page. +type SandboxPhase = 'empty' | 'deploying' | 'active' | 'stopping' | 'suspending' + +const PHASE_ORDER: SandboxPhase[] = ['empty', 'deploying', 'active', 'stopping', 'suspending'] + +const PHASE_DURATION_MS: Record = { + empty: [3000, 6000], + deploying: [2000, 4000], + active: [18000, 32000], + stopping: [800, 1600], + suspending: [3000, 5000], +} + +const STATUS_META: Record< + Exclude, + { label: string; dot: string; text: string } +> = { + deploying: { label: 'Deploying', dot: 'bg-warning animate-pulse', text: 'text-foreground' }, + active: { label: 'Active', dot: 'bg-brand', text: 'text-foreground' }, + stopping: { label: 'Stopping', dot: 'bg-warning', text: 'text-foreground-muted' }, + suspending: { label: 'Suspending', dot: 'bg-border-stronger', text: 'text-foreground-muted' }, +} + +const SANDBOX_RUNTIMES = ['node', 'deno'] as const + +// Names read like something an agent platform could generate per run +const SANDBOX_WORKLOADS = [ + 'brainstorm', + 'plan', + 'prototype', + 'demo', + 'explore', + 'research', + 'spike', + 'draft', + 'scaffold', + 'smoosh', + 'review', + 'support', + 'tests', + 'e2e', + 'build', + 'scrape', + 'crawl', + 'migrate', + 'seed', + 'bench', + 'index', + 'parse', + 'render', +] as const + +// The name each slot renders on the server and on first paint. Names are only +// ever re-rolled later, from the lifecycle effect, so hydration stays stable. +const SANDBOX_SLOTS = [ + { name: 'review-4f2a', size: '2 GB · 1 vCPU', delay: 0 }, + { name: 'plan-9be0', size: '2 GB · 1 vCPU', delay: 5500 }, + { name: 'scrape-7c31', size: '2 GB · 1 vCPU', delay: 9000 }, + { name: 'migrate-2d8a', size: '4 GB · 2 vCPU', delay: 3000 }, +] as const + +const PERSISTENT_SERVICES = [ + { name: 'api', runtime: 'node', size: '2 GB · 1 vCPU', since: '2mo ago' }, + { name: 'embeddings', runtime: 'dockerfile', size: '4 GB · 2 vCPU', since: '3d ago' }, +] as const + +function randomInRange([min, max]: [number, number]) { + return min + Math.random() * (max - min) +} + +function randomItem(items: readonly T[]) { + return items[Math.floor(Math.random() * items.length)] +} + +// Two sandboxes can legitimately run the same workload, so the run id — not +// the prefix — is what keeps names distinct on screen. +function randomSandboxName() { + const runId = Math.floor(Math.random() * 0x10000) + .toString(16) + .padStart(4, '0') + return `${randomItem(SANDBOX_WORKLOADS)}-${runId}` +} + +function formatElapsed(seconds: number) { + if (seconds < 60) return `${seconds}s` + return `${Math.floor(seconds / 60)}m${seconds % 60}s` +} + +function useSandboxLifecycle( + runtimes: readonly string[], + { name: initialName, delay: initialDelayMs }: (typeof SANDBOX_SLOTS)[number] +) { + const prefersReducedMotion = useReducedMotion() + const [phase, setPhase] = useState('empty') + const [elapsed, setElapsed] = useState(0) + const [runtime, setRuntime] = useState(runtimes[0]) + const [name, setName] = useState(initialName) + + // Drives the phase machine: schedule the next phase, then reschedule + // itself from there. Each deploy is a different sandbox, so the name and + // runtime are both re-rolled every time a build starts. + useEffect(() => { + if (prefersReducedMotion) return + + let timeoutId: ReturnType + + const advance = (current: SandboxPhase) => { + const next = PHASE_ORDER[(PHASE_ORDER.indexOf(current) + 1) % PHASE_ORDER.length] + if (next === 'deploying') { + setName(randomSandboxName()) + setRuntime(randomItem(runtimes)) + } + setPhase(next) + setElapsed(0) + timeoutId = setTimeout(() => advance(next), randomInRange(PHASE_DURATION_MS[next])) + } + + timeoutId = setTimeout(() => advance('empty'), initialDelayMs) + return () => clearTimeout(timeoutId) + }, [runtimes, initialDelayMs, prefersReducedMotion]) + + // Ticks the elapsed-time counter shown in the card footer while the + // sandbox is doing something worth timing. + useEffect(() => { + if (prefersReducedMotion || phase === 'empty' || phase === 'suspending') return + const interval = setInterval(() => setElapsed((value) => value + 1), 1000) + return () => clearInterval(interval) + }, [phase, prefersReducedMotion]) + + if (prefersReducedMotion) + return { phase: 'active' as const, elapsed: 96, runtime: runtimes[0], name: initialName } + return { phase, elapsed, runtime, name } +} + +function ComputeIcon({ dimmed = false }: { dimmed?: boolean }) { + return ( +
+ +
+ ) +} + +function ComputeCardShell({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function ComputeCardHeader({ + name, + runtime, + statusSlot, +}: { + name: string + runtime: string + statusSlot: React.ReactNode +}) { + return ( +
+
+ +
+ {name} + {runtime} +
+
+
{statusSlot}
+
+ ) +} + +function ComputeCardFooter({ + left, + right, + className, +}: { + left: string | React.ReactNode + right: string + className?: string +}) { + return ( +
+ {left} + {right} +
+ ) +} + +// Always-on production backend service: fixed status, no lifecycle. +function PersistentServiceCard({ + name, + runtime, + size, + since, +}: (typeof PERSISTENT_SERVICES)[number]) { + return ( + + + + Active + + } + /> + + + + Active + + } + right={since} + /> + + ) +} + +// Ephemeral sandbox: renders as a dashed empty slot until an agent +// spins it up, then crossfades into a live card that tracks its phase. +// Lifecycle state is lifted to the parent (see useSandboxLifecycle call +// sites in ComputeVisual) so the connector lines can react to it too. +function SandboxCard({ + name, + size, + phase, + elapsed, + runtime, +}: { + name: string + size: string + phase: SandboxPhase + elapsed: number + runtime: string +}) { + return ( + + {phase === 'empty' ? ( + + + + ) : ( + + + + + + + {STATUS_META[phase].label} + + + + } + /> + + + + + + {STATUS_META[phase].label} + + + + } + right={phase === 'suspending' ? '–' : formatElapsed(elapsed)} + /> + + + )} + + ) +} + +function ApplicationCard() { + return ( +
+ + + Application + +
+ ) +} + +function SourceCard({ icon: Icon, label }: { icon: LucideIcon; label: string }) { + return ( +
+ + + {label} + +
+ ) +} + +/* ------------------------------------------------------------------ * + * Connectors + * + * The dashed lines are drawn in a single SVG overlay instead of being + * baked into the layout, so they stay correct at any width: every card + * is measured, each connector is routed between two measured centers, + * and the whole layer is masked by the card rectangles. Masking is what + * lets a connector be anchored at a card's center — the line is trimmed + * exactly at the card's edge, so it reads as plugging into the card. + * ------------------------------------------------------------------ */ + +type NodeKey = + | 'app' + | 'api' + | 'embeddings' + | 'sandboxTopMid' + | 'sandboxBottomMid' + | 'sandboxTopRight' + | 'sandboxBottomRight' + | 'human' + | 'agent1' + | 'agent2' + +type Box = { x: number; y: number; width: number; height: number } +type Point = { x: number; y: number } +type Geometry = { width: number; height: number; boxes: Partial> } +type Connector = { id: string; d: string; active: boolean } + +const CORNER_RADIUS = 14 +const CARD_RADIUS = 8 + +// Measures every registered node relative to the container and re-measures +// on any resize (viewport, font load, a card changing size). +function useDiagramGeometry() { + const containerRef = useRef(null) + const nodesRef = useRef(new Map()) + const [geometry, setGeometry] = useState(null) + + const registerNode = useCallback( + (key: NodeKey) => (element: HTMLElement | null) => { + if (element) nodesRef.current.set(key, element) + else nodesRef.current.delete(key) + }, + [] + ) + + useEffect(() => { + const container = containerRef.current + if (!container) return + + const measure = () => { + const base = container.getBoundingClientRect() + const boxes: Partial> = {} + nodesRef.current.forEach((element, key) => { + const rect = element.getBoundingClientRect() + boxes[key] = { + x: rect.left - base.left, + y: rect.top - base.top, + width: rect.width, + height: rect.height, + } + }) + const next = { width: base.width, height: base.height, boxes } + // Bail out on no-op measurements: ResizeObserver fires once per + // observed element on setup, and each of those would re-render. + setGeometry((current) => + current && JSON.stringify(current) === JSON.stringify(next) ? current : next + ) + } + + measure() + const observer = new ResizeObserver(measure) + observer.observe(container) + nodesRef.current.forEach((element) => observer.observe(element)) + return () => observer.disconnect() + }, []) + + return { containerRef, registerNode, geometry } +} + +function distance(a: Point, b: Point) { + return Math.hypot(b.x - a.x, b.y - a.y) +} + +function towards(from: Point, to: Point, length: number): Point { + const total = distance(from, to) + if (total === 0) return from + return { + x: from.x + ((to.x - from.x) * length) / total, + y: from.y + ((to.y - from.y) * length) / total, + } +} + +// Orthogonal polyline with rounded corners — the classic diagram elbow. +function roundedPath(points: Point[], radius = CORNER_RADIUS) { + const waypoints = points.filter( + (point, index) => index === 0 || distance(points[index - 1], point) > 0.5 + ) + if (waypoints.length < 2) return '' + + let d = `M ${waypoints[0].x} ${waypoints[0].y}` + for (let index = 1; index < waypoints.length - 1; index++) { + const previous = waypoints[index - 1] + const corner = waypoints[index] + const next = waypoints[index + 1] + const cornerRadius = Math.min( + radius, + distance(previous, corner) / 2, + distance(corner, next) / 2 + ) + if (cornerRadius < 0.5) { + d += ` L ${corner.x} ${corner.y}` + continue + } + const entry = towards(corner, previous, cornerRadius) + const exit = towards(corner, next, cornerRadius) + d += ` L ${entry.x} ${entry.y} Q ${corner.x} ${corner.y} ${exit.x} ${exit.y}` + } + const last = waypoints[waypoints.length - 1] + return `${d} L ${last.x} ${last.y}` +} + +// Routes every connector from the measured layout: one line per column from +// its source card up into the bottom card of that column, plus the always-on +// feeding the application. Anchoring at card centers and masking the card +// rectangles is what makes a line read as plugging into the card. +function buildConnectors(geometry: Geometry, live: Record): Connector[] { + const { boxes } = geometry + const keys: NodeKey[] = [ + 'app', + 'api', + 'embeddings', + 'sandboxBottomMid', + 'sandboxBottomRight', + 'human', + 'agent1', + 'agent2', + ] + if (keys.some((key) => !boxes[key])) return [] + + const box = (key: NodeKey) => boxes[key] as Box + const top = (key: NodeKey) => box(key).y + const bottom = (key: NodeKey) => box(key).y + box(key).height + const cx = (key: NodeKey) => box(key).x + box(key).width / 2 + const cy = (key: NodeKey) => box(key).y + box(key).height / 2 + const center = (key: NodeKey): Point => ({ x: cx(key), y: cy(key) }) + const at = (x: number, y: number): Point => ({ x, y }) + + // Horizontal bands: the empty strips above and below the card grid, where + // each connector makes its sideways jog. + const sourceBand = (bottom('embeddings') + top('human')) / 2 + const appBand = (bottom('app') + top('api')) / 2 + + const connector = (id: string, points: Point[], active = true): Connector => ({ + id, + d: roundedPath(points), + active, + }) + + return [ + // Each source feeds its own column. + connector('human-embeddings', [ + center('human'), + at(cx('human'), sourceBand), + at(cx('embeddings'), sourceBand), + center('embeddings'), + ]), + connector( + 'agent1-sandboxBottomMid', + [ + center('agent1'), + at(cx('agent1'), sourceBand), + at(cx('sandboxBottomMid'), sourceBand), + center('sandboxBottomMid'), + ], + live.sandboxBottomMid + ), + connector( + 'agent2-sandboxBottomRight', + [ + center('agent2'), + at(cx('agent2'), sourceBand), + at(cx('sandboxBottomRight'), sourceBand), + center('sandboxBottomRight'), + ], + live.sandboxBottomRight + ), + + // The always-on column serves the application. + connector('api-app', [ + center('api'), + at(cx('api'), appBand), + at(cx('app'), appBand), + center('app'), + ]), + ] +} + +// Faint dashed base for every connector, plus a brighter overlay that marches +// along the path (see .flow) and fades out while the workload it feeds is idle. +function ConnectorLayer({ + geometry, + live, +}: { + geometry: Geometry | null + live: Record +}) { + const maskId = `compute-connectors-mask-${useId()}` + if (!geometry) return null + + const { width, height, boxes } = geometry + const connectors = buildConnectors(geometry, live) + if (!connectors.length) return null + + return ( + + ) +} + +export function ComputeVisual() { + // Lifecycle state for the 4 sandbox slots, lifted up here so the connector + // lines below can react to whichever sandboxes they feed. + const sandboxTopMid = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[0]) + const sandboxTopRight = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[1]) + const sandboxBottomMid = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[2]) + const sandboxBottomRight = useSandboxLifecycle(SANDBOX_RUNTIMES, SANDBOX_SLOTS[3]) + + const { containerRef, registerNode, geometry } = useDiagramGeometry() + + // A connector only flows while the sandbox it feeds is actually running. + const isLive = (phase: SandboxPhase) => phase !== 'empty' && phase !== 'suspending' + const live = { + sandboxTopMid: isLive(sandboxTopMid.phase), + sandboxBottomMid: isLive(sandboxBottomMid.phase), + sandboxTopRight: isLive(sandboxTopRight.phase), + sandboxBottomRight: isLive(sandboxBottomRight.phase), + } + + return ( + +
+ + A diagram showing humans and agents connecting to Supabase Compute. Two always-on backend + services serve the application; agents spin up short-lived sandboxes on demand, which + build, run, and suspend automatically. + + + + +
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/DeploySection.tsx b/apps/www/app/(products)/compute/_components/DeploySection.tsx new file mode 100644 index 00000000000..3c2aadb192b --- /dev/null +++ b/apps/www/app/(products)/compute/_components/DeploySection.tsx @@ -0,0 +1,91 @@ +'use client' + +import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn' +import { AnimatePresence, motion, useInView, useReducedMotion } from 'framer-motion' +import Link from 'next/link' +import { useEffect, useRef, useState } from 'react' +import { Button } from 'ui' + +const TERMINAL_LINES = [ + { text: '$ supabase compute deploy my-app', delay: 0 }, + { text: 'Building from Dockerfile...', delay: 0.3, dim: true }, + { text: "Provisioning 2 GB / 1 vCPU in your database's region...", delay: 0.6, dim: true }, + { text: 'Issuing scoped project credentials...', delay: 0.9, dim: true }, + { text: '', delay: 1.1 }, + { text: '✓ Service deployed:', delay: 1.2, accent: true }, + { text: ' https://your-project.supabase.co/compute/my-app', delay: 1.3, accent: true }, +] + +function TerminalAnimation() { + const ref = useRef(null) + const inView = useInView(ref, { once: true, amount: 0.5 }) + const prefersReducedMotion = useReducedMotion() + const [visibleCount, setVisibleCount] = useState(0) + + useEffect(() => { + if (!inView) return + + // Show the finished output straight away rather than typing it out. + if (prefersReducedMotion) { + setVisibleCount(TERMINAL_LINES.length) + return + } + + const timeouts: ReturnType[] = [] + TERMINAL_LINES.forEach((line, i) => { + timeouts.push(setTimeout(() => setVisibleCount(i + 1), line.delay * 1000)) + }) + return () => timeouts.forEach(clearTimeout) + }, [inView, prefersReducedMotion]) + + return ( +
+
+ Terminal +
+
+ + {TERMINAL_LINES.slice(0, visibleCount).map((line, i) => ( + + {line.text || ' '} + + ))} + +
+
+ ) +} + +export function DeploySection() { + return ( + +
+
+ + Deployment + +

+ Deploy from + wherever you work +

+

+ The same deployment surface serves humans and agents: one command in a terminal, one + tool call in an agent session, one request against the API. +

+ +
+ +
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/FAQSection.tsx b/apps/www/app/(products)/compute/_components/FAQSection.tsx new file mode 100644 index 00000000000..175d2c99007 --- /dev/null +++ b/apps/www/app/(products)/compute/_components/FAQSection.tsx @@ -0,0 +1,70 @@ +'use client' + +import ReactMarkdown from 'react-markdown' +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'ui' + +const faqs = [ + { + question: 'How does Compute relate to Edge Functions?', + answer: + "Compute complements Edge Functions, which remain fully supported and best for short-lived work like webhooks and lightweight APIs. Compute handles what they can't: long-running jobs, background workers, and heavier workloads with no wall-clock limits. If a function outgrows Edge Functions, you can migrate it to Compute without code changes.", + }, + { + question: 'Which languages and runtimes are supported?', + answer: + 'In Private Alpha, Node, Deno, and any Dockerfile are supported — no wrapper scripts or extra orchestration code required. Bun and Python are on the way; until they land, a Dockerfile covers anything else you need to run.', + }, + { + question: 'Can a service run indefinitely?', + answer: + 'There are no wall-clock limits, so jobs and pipelines run as long as the work takes. For HTTP services, Compute suspends automatically when idle on network I/O and resumes in under a second, so a service configured as always-on behaves like one without billing you for idle time.', + }, + { + question: 'Where does Compute run?', + answer: + "During Private Alpha, Compute is only available in us-west-2, with more regions rolling out over time until every Supabase region is covered. Sandboxes and services deploy to your primary database's region — that's where writes happen, and it keeps behavior predictable. They can still read from replicas via the load-balanced connection endpoint, so replica capacity isn't wasted. Pinning a workload to a specific replica region is on the roadmap.", + }, + { + question: 'Can I attach a persistent disk?', + answer: + 'Sandboxes and services are designed to scale to zero and restart on demand, so the local filesystem is fast scratch space, not durable storage. For state that needs to persist, use your Postgres database for structured data and Supabase Storage for files — both reachable with near-zero intra-region latency and no data-transfer tax. Mountable persistent disks are on the roadmap.', + }, + { + question: 'How will pricing work?', + answer: + 'Compute bills per compute-hour based on the instance size you choose, with no per-request metering. Idle sandboxes and services suspend automatically, so you pay nothing while they sleep. Traffic between a workload and its own database is free. There is no separate subscription — usage appears on your existing Supabase invoice with the same spend caps and credits. Final rates will be published before general availability.', + }, + { + question: 'How do I get access?', + answer: + 'Compute is in Private Alpha. Join the waitlist and we will send invites in waves as capacity grows. Feedback from alpha participants directly shapes the product ahead of public launch.', + }, +] + +export function FAQSection() { + return ( +
+
+

Frequently asked questions

+
+ + {faqs.map((faq, i) => ( +
+ + + {faq.question} + + +
+ {faq.answer} +
+
+
+
+ ))} +
+
+
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/Hero.tsx b/apps/www/app/(products)/compute/_components/Hero.tsx new file mode 100644 index 00000000000..ee6b99a77ee --- /dev/null +++ b/apps/www/app/(products)/compute/_components/Hero.tsx @@ -0,0 +1,33 @@ +import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn' +import Link from 'next/link' +import { Badge, Button } from 'ui' + +export function Hero() { + return ( + +
+
+ Private Alpha +
+
+

+ Compute + for agentic workloads +

+

+ Agent sandboxes and always-on backend services running inside your Supabase project — + next to your data, behind your auth. +

+
+
+ + {/* */} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/Highlights.tsx b/apps/www/app/(products)/compute/_components/Highlights.tsx new file mode 100644 index 00000000000..d1a4fef5d20 --- /dev/null +++ b/apps/www/app/(products)/compute/_components/Highlights.tsx @@ -0,0 +1,41 @@ +import { FeatureItem, type Feature } from '~/components/FeatureItem' +import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn' +import { Boxes, Database, DollarSign, Terminal } from 'lucide-react' + +const highlights: Feature[] = [ + { + icon: Boxes, + heading: 'One primitive, every workload', + subheading: 'Ephemeral sandboxes and always-on HTTP services, one primitive.', + }, + { + icon: Terminal, + heading: 'Any language', + subheading: 'Node, Deno, or any Dockerfile.', + }, + { + icon: Database, + heading: 'Next to your database', + subheading: 'Same network as Postgres — single-digit millisecond queries.', + }, + { + icon: DollarSign, + heading: 'Scale to zero', + subheading: + 'Services suspend when idle and resume in under a second. You pay nothing while they sleep.', + }, +] + +export function Highlights() { + return ( +
+ +
    + {highlights.map((highlight) => ( + + ))} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/TrustBoundarySection.tsx b/apps/www/app/(products)/compute/_components/TrustBoundarySection.tsx new file mode 100644 index 00000000000..424514d02a0 --- /dev/null +++ b/apps/www/app/(products)/compute/_components/TrustBoundarySection.tsx @@ -0,0 +1,70 @@ +import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn' +import { EyeOff, KeyRound, Network, Plug, ScanSearch, ShieldCheck } from 'lucide-react' + +const features = [ + { + title: 'Short-lived credentials', + paragraph: 'Access runs through Supabase Auth with short-lived keys, not standing secrets.', + icon: , + }, + { + title: 'Zero-config data access', + paragraph: 'Reach your database and Storage with the permissions your key already carries.', + icon: , + }, + { + title: 'Per-workload firewalls', + paragraph: 'Define which external endpoints and ports each workload can reach.', + icon: , + }, + { + title: 'Cloaked secrets', + paragraph: 'Scope secrets per workload, and cloak values so your code never sees them.', + icon: , + }, + { + title: 'Patched automatically', + paragraph: 'Kernel-level security patches roll out automatically. Nothing to do on your part.', + icon: , + }, + { + title: 'Dependency scanning', + paragraph: 'Dependencies are scanned at runtime, with alerts as new vulnerabilities land.', + icon: , + }, +] + +export function TrustBoundarySection() { + return ( + +
+ + Security + +

+ One trust boundary, + preconfigured +

+

+ Every sandbox and service inherits your project's auth, roles, and permissions the moment + it starts, running in its own microVM with dedicated CPU and memory. Agents get a sandbox + that is already scoped to the data it is allowed to touch — nothing more. +

+
+
+ {features.map((feature) => ( +
+
+ {feature.icon} +
+

{feature.title}

+

{feature.paragraph}

+
+ ))} +
+
+ ) +} diff --git a/apps/www/app/(products)/compute/_components/WaitlistForm.tsx b/apps/www/app/(products)/compute/_components/WaitlistForm.tsx new file mode 100644 index 00000000000..c1e1bbc030f --- /dev/null +++ b/apps/www/app/(products)/compute/_components/WaitlistForm.tsx @@ -0,0 +1,263 @@ +'use client' + +import { CircleAlert } from 'lucide-react' +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { + Button, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Separator, + TextArea, +} from 'ui' +import { Alert } from 'ui/src/components/shadcn/ui/alert' + +interface FormData { + firstName: string + lastName: string + email: string + company: string + useCase: string + details: string +} + +const defaultFormValue: FormData = { + firstName: '', + lastName: '', + email: '', + company: '', + useCase: '', + details: '', +} + +const isValidEmail = (email: string): boolean => { + const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/ + return emailPattern.test(email) +} + +export function WaitlistForm() { + const [formData, setFormData] = useState(defaultFormValue) + const [honeypot, setHoneypot] = useState('') + const [errors, setErrors] = useState<{ [key: string]: string }>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [success, setSuccess] = useState(false) + const [startTime, setStartTime] = useState(0) + + useEffect(() => { + setStartTime(Date.now()) + }, []) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setErrors({}) + setFormData((prev) => ({ ...prev, [name]: value })) + } + + const validate = (): boolean => { + const newErrors: { [key: string]: string } = {} + if (!formData.firstName) newErrors.firstName = 'This field is required' + if (!formData.lastName) newErrors.lastName = 'This field is required' + if (!formData.email) newErrors.email = 'This field is required' + else if (!isValidEmail(formData.email)) newErrors.email = 'Invalid email address' + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + // Spam prevention: reject if submitted implausibly fast. + if ((Date.now() - startTime) / 1000 < 3) { + setErrors({ general: 'Submission too fast. Please try again.' }) + return + } + + if (!validate()) return + + setIsSubmitting(true) + + try { + const response = await fetch('/api-v2/submit-form-compute-waitlist', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...formData, honeypot }), + }) + + if (response.ok) { + setSuccess(true) + } else { + const errorData = await response.json() + setErrors({ general: errorData.message ?? 'Submission failed. Please try again.' }) + } + } catch { + setErrors({ general: 'An unexpected error occurred. Please try again.' }) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+ {/* Always mounted so assistive tech announces submission results as they change. */} +
+ {success + ? "You're on the list. We'll reach out as invites roll out during the Private Alpha." + : (errors.general ?? '')} +
+ {success ? ( +
+

You're on the list

+

+ We'll reach out as invites roll out during the Private Alpha. +

+
+ ) : ( +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +