feat: compute landing page (#50899)

Compute landing page.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Francesco Sansalvadore authored and GitHub committed 2026-10-02 16:14:13 +02:00
1 parent 1f5d10e696
commit 4617b6f4cb
25 files changed
+1826 -33

No files matched your search

@@ -0,0 +1,24 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import Link from 'next/link'
import { WaitlistForm } from './WaitlistForm'
export function CTASection() {
return (
<SectionContainerWithCn height="none">
<div className="flex flex-col items-center text-center gap-10 py-24 md:py-32">
<div className="flex flex-col items-center gap-4">
<h2 className="text-2xl md:text-4xl text-foreground">
Run your next workload
<span className="block text-foreground-lighter">next to your data</span>
</h2>
<p className="text-foreground-lighter text-sm max-w-md">
Compute is in Private Alpha. Join the waitlist and we&apos;ll reach out as invites roll
out.
</p>
</div>
<WaitlistForm />
</div>
</SectionContainerWithCn>
)
}
@@ -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 (
<div className="overflow-x-clip">
<section className="border-t border-border" aria-label="Hero">
<Hero />
</section>
<section id="workloads" className="border-t border-border" aria-label="Workload shapes">
<WorkloadsSection />
</section>
<section id="highlights" className="space-y-8 md:space-y-16" aria-label="Compute highlights">
<ComputeVisual />
<Highlights />
</section>
<section id="security" className="border-t border-border" aria-label="Security model">
<TrustBoundarySection />
</section>
<section id="deploy" className="border-t border-border" aria-label="Deployment options">
<DeploySection />
</section>
<section id="faq" className="border-t border-border" aria-label="Frequently asked questions">
<FAQSection />
</section>
<section id="waitlist" className="border-t border-border" aria-label="Join the waitlist">
<CTASection />
</section>
</div>
)
}
@@ -0,0 +1,42 @@
interface ComputeLogoProps {
size?: number
className?: string
}
export function ComputeLogo({ size = 18, className }: ComputeLogoProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M15.75 12V5.99999C15.7497 5.73694 15.6803 5.4786 15.5487 5.25086C15.417 5.02312 15.2278 4.83401 15 4.70249L9.75 1.70249C9.52197 1.57084 9.2633 1.50153 9 1.50153C8.7367 1.50153 8.47803 1.57084 8.25 1.70249L3 4.70249C2.7722 4.83401 2.58299 5.02312 2.45135 5.25086C2.31971 5.4786 2.25027 5.73694 2.25 5.99999V12C2.25027 12.263 2.31971 12.5214 2.45135 12.7491C2.58299 12.9769 2.7722 13.166 3 13.2975L8.25 16.2975C8.47803 16.4291 8.7367 16.4985 9 16.4985C9.2633 16.4985 9.52197 16.4291 9.75 16.2975L15 13.2975C15.2278 13.166 15.417 12.9769 15.5487 12.7491C15.6803 12.5214 15.7497 12.263 15.75 12Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13 10.6011V7.4004C12.9998 7.26009 12.9587 7.12227 12.8807 7.00079C12.8027 6.8793 12.6906 6.77842 12.5556 6.70826L9.44444 5.10793C9.30932 5.0377 9.15603 5.00073 9 5.00073C8.84397 5.00073 8.69068 5.0377 8.55556 5.10793L5.44444 6.70826C5.30945 6.77842 5.19732 6.8793 5.11932 7.00079C5.04131 7.12227 5.00016 7.26009 5 7.4004V10.6011C5.00016 10.7414 5.04131 10.8792 5.11932 11.0007C5.19732 11.1222 5.30945 11.223 5.44444 11.2932L8.55556 12.8935C8.69068 12.9638 8.84397 13.0007 9 13.0007C9.15603 13.0007 9.30932 12.9638 9.44444 12.8935L12.5556 11.2932C12.6906 11.223 12.8027 11.1222 12.8807 11.0007C12.9587 10.8792 12.9998 10.7414 13 10.6011Z"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
<path
d="M6 7.00073L9 9.00073L12 7.00073"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
<path
d="M9 12.5V9.00073"
stroke="currentColor"
strokeLinecap="square"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -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<SandboxPhase, [number, number]> = {
empty: [3000, 6000],
deploying: [2000, 4000],
active: [18000, 32000],
stopping: [800, 1600],
suspending: [3000, 5000],
}
const STATUS_META: Record<
Exclude<SandboxPhase, 'empty'>,
{ 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<T>(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<SandboxPhase>('empty')
const [elapsed, setElapsed] = useState(0)
const [runtime, setRuntime] = useState<string>(runtimes[0])
const [name, setName] = useState<string>(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<typeof setTimeout>
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 (
<div
className={cn(
'hidden sm:flex h-6 w-6 md:h-7 md:w-7 shrink-0 items-center justify-center rounded-md border border-border',
dimmed ? 'text-foreground-muted opacity-30' : 'text-foreground-lighter'
)}
>
<ComputeLogo size={16} />
</div>
)
}
function ComputeCardShell({ children }: { children: React.ReactNode }) {
return (
<div className="flex w-full flex-col rounded-lg border border-border bg-surface-75 overflow-hidden">
{children}
</div>
)
}
function ComputeCardHeader({
name,
runtime,
statusSlot,
}: {
name: string
runtime: string
statusSlot: React.ReactNode
}) {
return (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 px-3 py-2.5">
<div className="flex items-center gap-2">
<ComputeIcon />
<div className="flex min-w-0 flex-col leading-tight">
<span className="truncate text-xs text-foreground">{name}</span>
<span className="truncate font-mono text-[11px] text-foreground-muted">{runtime}</span>
</div>
</div>
<div className="hidden sm:ml-auto sm:flex shrink-0 items-center gap-1.5">{statusSlot}</div>
</div>
)
}
function ComputeCardFooter({
left,
right,
className,
}: {
left: string | React.ReactNode
right: string
className?: string
}) {
return (
<div
className={cn(
'bg-surface-200 flex flex-col sm:flex-row sm:items-center sm:justify-between border-t border-border px-3 py-2 font-mono text-xs text-foreground-muted',
className
)}
>
<span className="flex items-center gap-1.5">{left}</span>
<span>{right}</span>
</div>
)
}
// Always-on production backend service: fixed status, no lifecycle.
function PersistentServiceCard({
name,
runtime,
size,
since,
}: (typeof PERSISTENT_SERVICES)[number]) {
return (
<ComputeCardShell>
<ComputeCardHeader
name={name}
runtime={runtime}
statusSlot={
<>
<span className="h-1.5 w-1.5 rounded-full bg-brand" aria-hidden />
<span className="text-xs text-foreground">Active</span>
</>
}
/>
<ComputeCardFooter className="hidden sm:flex" left={size} right={since} />
<ComputeCardFooter
className="sm:hidden"
left={
<>
<span className="h-1.5 w-1.5 rounded-full bg-brand" aria-hidden />
<span className="text-xs text-foreground">Active</span>
</>
}
right={since}
/>
</ComputeCardShell>
)
}
// 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 (
<AnimatePresence mode="popLayout" initial={false}>
{phase === 'empty' ? (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="flex h-full items-center justify-center rounded-lg border border-dashed border-border"
>
<ComputeIcon dimmed />
</motion.div>
) : (
<motion.div
key="card"
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.96 }}
transition={{ duration: 0.3 }}
>
<ComputeCardShell>
<ComputeCardHeader
name={name}
runtime={runtime}
statusSlot={
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={phase}
initial={{ opacity: 0, y: -2 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 2 }}
transition={{ duration: 0.2 }}
className="flex items-center gap-1.5"
>
<span
className={cn('h-1.5 w-1.5 rounded-full', STATUS_META[phase].dot)}
aria-hidden
/>
<span className={cn('text-xs', STATUS_META[phase].text)}>
{STATUS_META[phase].label}
</span>
</motion.span>
</AnimatePresence>
}
/>
<ComputeCardFooter
className="hidden sm:flex"
left={size}
right={phase === 'suspending' ? '–' : formatElapsed(elapsed)}
/>
<ComputeCardFooter
className="sm:hidden"
left={
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={phase}
initial={{ opacity: 0, y: -2 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 2 }}
transition={{ duration: 0.2 }}
className="flex items-center gap-1.5"
>
<span
className={cn('h-1.5 w-1.5 rounded-full', STATUS_META[phase].dot)}
aria-hidden
/>
<span className={cn('text-xs', STATUS_META[phase].text)}>
{STATUS_META[phase].label}
</span>
</motion.span>
</AnimatePresence>
}
right={phase === 'suspending' ? '–' : formatElapsed(elapsed)}
/>
</ComputeCardShell>
</motion.div>
)}
</AnimatePresence>
)
}
function ApplicationCard() {
return (
<div className="flex items-center gap-2 rounded-lg border border-foreground-muted/30 border-dashed px-4 py-2.5">
<Database className="h-4 w-4 text-foreground-lighter" strokeWidth={1.5} aria-hidden />
<span className="font-mono text-xs uppercase tracking-widest text-foreground-lighter">
Application
</span>
</div>
)
}
function SourceCard({ icon: Icon, label }: { icon: LucideIcon; label: string }) {
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-foreground-muted/30 border-dashed sm:min-w-32 px-4 py-2 sm:py-4 gap-1 sm:gap-2">
<Icon className="h-5 w-5 text-foreground-muted" strokeWidth={1.5} aria-hidden />
<span className="font-mono text-xs uppercase tracking-widest text-foreground-muted">
{label}
</span>
</div>
)
}
/* ------------------------------------------------------------------ *
* 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<Record<NodeKey, Box>> }
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<HTMLElement | null>(null)
const nodesRef = useRef(new Map<NodeKey, HTMLElement>())
const [geometry, setGeometry] = useState<Geometry | null>(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<Record<NodeKey, Box>> = {}
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<string, boolean>): 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<string, boolean>
}) {
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 (
<svg
className="pointer-events-none absolute inset-0 h-full w-full"
viewBox={`0 0 ${width} ${height}`}
fill="none"
aria-hidden="true"
>
<mask id={maskId} maskUnits="userSpaceOnUse" x="0" y="0" width={width} height={height}>
<rect x="0" y="0" width={width} height={height} fill="white" />
{Object.entries(boxes).map(([key, node]) => (
<rect
key={key}
x={node.x}
y={node.y}
width={node.width}
height={node.height}
rx={CARD_RADIUS}
fill="black"
/>
))}
</mask>
<g mask={`url(#${maskId})`}>
{connectors.map((connector) => (
<g key={connector.id}>
<path
d={connector.d}
className="text-border-muted"
stroke="currentColor"
strokeWidth="1"
strokeDasharray="2 4"
/>
<path
d={connector.d}
className={cn(
'text-foreground-muted/50 transition-opacity duration-100',
styles.flow
)}
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
style={{ opacity: connector.active ? 1 : 0 }}
/>
</g>
))}
</g>
</svg>
)
}
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 (
<SectionContainerWithCn>
<figure ref={containerRef} className="relative w-full max-w-3xl mx-auto">
<span className="sr-only">
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.
</span>
<ConnectorLayer geometry={geometry} live={live} />
<div className="relative grid grid-cols-3 gap-x-2" aria-hidden="true">
<div className="flex col-span-full justify-center">
<div ref={registerNode('app')}>
<ApplicationCard />
</div>
</div>
<div className="col-span-full h-12" />
<div ref={registerNode('api')} className="sm:pr-2">
<PersistentServiceCard {...PERSISTENT_SERVICES[0]} />
</div>
<div ref={registerNode('sandboxTopMid')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[0].size} {...sandboxTopMid} />
</div>
<div ref={registerNode('sandboxTopRight')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[1].size} {...sandboxTopRight} />
</div>
<div className="col-span-full h-2" />
<div ref={registerNode('embeddings')} className="sm:pr-2">
<PersistentServiceCard {...PERSISTENT_SERVICES[1]} />
</div>
<div ref={registerNode('sandboxBottomMid')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[2].size} {...sandboxBottomMid} />
</div>
<div ref={registerNode('sandboxBottomRight')} className="h-full">
<SandboxCard size={SANDBOX_SLOTS[3].size} {...sandboxBottomRight} />
</div>
<div className="col-span-full h-12" />
{/* Sources */}
<div className="col-span-full flex justify-center gap-2">
<div ref={registerNode('human')}>
<SourceCard icon={User} label="Human" />
</div>
<div ref={registerNode('agent1')}>
<SourceCard icon={Bot} label="Agent 1" />
</div>
<div ref={registerNode('agent2')}>
<SourceCard icon={Bot} label="Agent 2" />
</div>
</div>
</div>
</figure>
</SectionContainerWithCn>
)
}
@@ -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<HTMLDivElement>(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<typeof setTimeout>[] = []
TERMINAL_LINES.forEach((line, i) => {
timeouts.push(setTimeout(() => setVisibleCount(i + 1), line.delay * 1000))
})
return () => timeouts.forEach(clearTimeout)
}, [inView, prefersReducedMotion])
return (
<div ref={ref} className="w-full rounded-lg border border-border bg-surface-75 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-2 border-b border-border bg-surface-200">
<span className="ml-2 text-xs text-foreground-muted font-mono uppercase">Terminal</span>
</div>
<div className="px-4 md:px-6 font-mono text-xs md:text-sm leading-relaxed h-[240px] flex flex-col justify-start items-start text-left pt-4">
<AnimatePresence>
{TERMINAL_LINES.slice(0, visibleCount).map((line, i) => (
<motion.div
key={i}
initial={prefersReducedMotion ? false : { opacity: 0, y: 1 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.2 }}
className={
line.accent ? 'text-brand' : line.dim ? 'text-foreground-light' : 'text-foreground'
}
>
{line.text || ' '}
</motion.div>
))}
</AnimatePresence>
</div>
</div>
)
}
export function DeploySection() {
return (
<SectionContainerWithCn spacing="sections">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-start">
<div className="flex flex-col items-start gap-4">
<span className="text-foreground-muted font-mono text-xs uppercase tracking-widest">
Deployment
</span>
<h2 className="text-2xl md:text-4xl text-foreground">
Deploy from
<span className="text-foreground-lighter block">wherever you work</span>
</h2>
<p className="text-foreground-lighter text-sm lg:text-base">
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.
</p>
<Button asChild>
<Link href="#waitlist">Join the waitlist</Link>
</Button>
</div>
<TerminalAnimation />
</div>
</SectionContainerWithCn>
)
}
@@ -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 (
<div className="mx-auto max-w-5xl">
<div className="sm:py-18 mx-auto px-6 py-16 md:py-24 lg:px-16 lg:py-24 xl:px-20">
<h2 className="h3 text-center">Frequently asked questions</h2>
<div className="my-16">
<Accordion type="multiple" className="text-foreground-light">
{faqs.map((faq, i) => (
<div className="border-b py-2" key={i}>
<AccordionItem value={`faq--${i.toString()}`} className="border-none">
<AccordionTrigger>
<span className="text-foreground text-left">{faq.question}</span>
</AccordionTrigger>
<AccordionContent>
<div className="prose text-foreground-lighter">
<ReactMarkdown>{faq.answer}</ReactMarkdown>
</div>
</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
</div>
</div>
</div>
)
}
@@ -0,0 +1,33 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import Link from 'next/link'
import { Badge, Button } from 'ui'
export function Hero() {
return (
<SectionContainerWithCn spacing="sections">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4 -mt-8">
<Badge variant="warning">Private Alpha</Badge>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-end">
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
<span className="flex items-center gap-3">Compute</span>
<span className="text-foreground-lighter block">for agentic workloads</span>
</h1>
<p className="text-foreground-lighter text-sm lg:text-base text-balance">
Agent sandboxes and always-on backend services running inside your Supabase project —
next to your data, behind your auth.
</p>
</div>
<div className="flex items-center gap-2 mt-2 lg:mt-4">
<Button asChild size="medium">
<Link href="#waitlist">Join the waitlist</Link>
</Button>
{/* <Button asChild size="medium" variant="outline">
<Link href="#waitlist">View announcement</Link>
</Button> */}
</div>
</div>
</SectionContainerWithCn>
)
}
@@ -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 (
<div>
<SectionContainerWithCn height="none" className="mb-8 md:mb-24">
<ul className="grid grid-cols-1 gap-4 gap-y-10 md:grid-cols-2 md:gap-12 xl:grid-cols-4">
{highlights.map((highlight) => (
<FeatureItem feature={highlight} key={highlight.heading} />
))}
</ul>
</SectionContainerWithCn>
</div>
)
}
@@ -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: <KeyRound className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Zero-config data access',
paragraph: 'Reach your database and Storage with the permissions your key already carries.',
icon: <Plug className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Per-workload firewalls',
paragraph: 'Define which external endpoints and ports each workload can reach.',
icon: <Network className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Cloaked secrets',
paragraph: 'Scope secrets per workload, and cloak values so your code never sees them.',
icon: <EyeOff className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Patched automatically',
paragraph: 'Kernel-level security patches roll out automatically. Nothing to do on your part.',
icon: <ShieldCheck className="w-5 h-5 stroke-[1.4px]" />,
},
{
title: 'Dependency scanning',
paragraph: 'Dependencies are scanned at runtime, with alerts as new vulnerabilities land.',
icon: <ScanSearch className="w-5 h-5 stroke-[1.4px]" />,
},
]
export function TrustBoundarySection() {
return (
<SectionContainerWithCn spacing="sections">
<div className="flex flex-col gap-4 max-w-xl">
<span className="text-foreground-muted font-mono text-xs uppercase tracking-widest">
Security
</span>
<h2 className="text-2xl md:text-4xl text-foreground">
One trust boundary,
<span className="text-foreground-lighter block">preconfigured</span>
</h2>
<p className="text-foreground-lighter text-sm lg:text-base">
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.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{features.map((feature) => (
<div
key={feature.title}
className="bg-surface-75 border border-border rounded-lg p-6 flex flex-col gap-3"
>
<div className="text-foreground-lighter" aria-hidden>
{feature.icon}
</div>
<h3 className="text-foreground text-base font-medium">{feature.title}</h3>
<p className="text-foreground-lighter text-sm">{feature.paragraph}</p>
</div>
))}
</div>
</SectionContainerWithCn>
)
}
@@ -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<FormData>(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<HTMLInputElement | HTMLTextAreaElement>) => {
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 (
<div className="w-full max-w-lg">
{/* Always mounted so assistive tech announces submission results as they change. */}
<div role="status" aria-live="polite" className="sr-only">
{success
? "You're on the list. We'll reach out as invites roll out during the Private Alpha."
: (errors.general ?? '')}
</div>
{success ? (
<div className="border border-border rounded-lg bg-surface-75 p-6 md:p-8 w-full flex flex-col items-center gap-2 text-center">
<p className="text-foreground font-medium">You&apos;re on the list</p>
<p className="text-foreground-lighter text-sm">
We&apos;ll reach out as invites roll out during the Private Alpha.
</p>
</div>
) : (
<form
onSubmit={handleSubmit}
className="border border-border rounded-lg bg-surface-75 p-6 md:p-8 w-full max-w-lg flex flex-col gap-4 text-left"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<Label
htmlFor="firstName"
className="text-foreground-light flex justify-between text-left"
>
First name
{errors.firstName && (
<span className="text-foreground-muted text-xs">{errors.firstName}</span>
)}
</Label>
<Input
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Jane"
/>
</div>
<div className="flex flex-col gap-2">
<Label
htmlFor="lastName"
className="text-foreground-light flex justify-between text-left"
>
Last name
{errors.lastName && (
<span className="text-foreground-muted text-xs">{errors.lastName}</span>
)}
</Label>
<Input
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Doe"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-foreground-light flex justify-between text-left">
Work email
{errors.email && (
<span className="text-foreground-muted text-xs">{errors.email}</span>
)}
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="jane@company.com"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="company" className="text-foreground-light text-left">
Company
</Label>
<Input
id="company"
name="company"
value={formData.company}
onChange={handleChange}
placeholder="Company name"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="useCase" className="text-foreground-light text-left">
What will you run on Compute?
</Label>
<Select
value={formData.useCase}
onValueChange={(value) => setFormData((prev) => ({ ...prev, useCase: value }))}
>
<SelectTrigger id="useCase">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sandboxes">Sandboxes for AI agents</SelectItem>
<SelectItem value="services">Backend services</SelectItem>
<SelectItem value="both">Both</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="details" className="text-foreground-light text-left">
Tell us more
</Label>
<TextArea
id="details"
name="details"
value={formData.details}
onChange={handleChange}
placeholder="What are you building, and what would you run on Compute?"
rows={3}
/>
</div>
{/* Honeypot: hidden from real users, catches basic bots. */}
<input
type="text"
name="honeypot"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
className="hidden"
/>
<Separator />
<Button type="submit" block size="medium" loading={isSubmitting} disabled={isSubmitting}>
Join the waitlist
</Button>
<p className="text-foreground-lighter text-xs text-left">
By submitting this form, you acknowledge that Supabase Compute is offered as a private
alpha preview, provided on an "as is" and "as available" basis without warranties of any
kind. It is intended for internal evaluation only and should not be used to serve
production workloads or your own end customers. Supabase may modify, suspend, or
discontinue the preview at any time without notice, and there is no guarantee it will
become a generally available product. You confirm you are authorized to submit this
request on behalf of your organization, and that you have read and understood our{' '}
<Link href="/privacy" className="text-foreground hover:underline">
Privacy Policy
</Link>
.
</p>
{errors.general && (
<Alert role="presentation" className="flex gap-2 text-foreground text-sm">
<CircleAlert className="w-3 h-3 shrink-0" /> <span>{errors.general}</span>
</Alert>
)}
</form>
)}
</div>
)
}
@@ -0,0 +1,105 @@
import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
import type { LucideIcon } from 'lucide-react'
import {
Bot,
Container,
Infinity as InfinityIcon,
Pause,
Server,
Shield,
Terminal,
TrendingUp,
} from 'lucide-react'
type WorkloadFeature = {
icon: LucideIcon
text: string
}
type Workload = {
label: string
title: string
paragraph: string
features: WorkloadFeature[]
}
const workloads: Workload[] = [
{
label: 'Sandboxes',
title: 'Isolated environments for agent code',
paragraph:
'Execute code in a secure, isolated environment — one per task, per session, or per agent.',
features: [
{ icon: Shield, text: 'Per-session environment for executing code' },
{ icon: Bot, text: 'Provision programmatically via the API or MCP server' },
{ icon: Pause, text: 'Suspend and resume with state preserved' },
],
},
{
label: 'Services',
title: 'Always-on backends in any language',
paragraph:
'Transcription pipelines, embedding jobs, agent frameworks, and APIs — running as long as the work takes.',
features: [
{ icon: TrendingUp, text: 'Auto-scaling for traffic surges' },
{ icon: InfinityIcon, text: 'No wall-clock limits on execution' },
{ icon: Container, text: 'Any runtime, static binary, or Dockerfile' },
{ icon: Server, text: 'HTTP services, background jobs, and long-running pipelines' },
],
},
]
export function WorkloadsSection() {
return (
<SectionContainerWithCn spacing="sections">
<div className="flex flex-col gap-4">
<span className="text-foreground-muted font-mono text-xs uppercase tracking-widest">
Workloads
</span>
<div className="flex flex-col md:grid grid-cols-2 items-end gap-2">
<h2 className="text-2xl md:text-4xl text-foreground">
One runtime,
<span className="text-foreground-lighter block">two shapes of work</span>
</h2>
<p className="text-foreground-lighter text-sm lg:text-base text-pretty">
Sandboxes, APIs, and background jobs usually mean three vendors and a database somewhere
else. Compute runs them all in Supabase alongside your application backend.
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{workloads.map((workload) => (
<div
key={workload.label}
className="bg-surface-75 border border-border rounded-lg p-6 md:p-8 flex flex-col gap-4"
>
<span className="text-foreground-lighter font-mono text-xs uppercase tracking-widest">
{workload.label}
</span>
<div className="flex flex-col gap-1">
<h3 className="text-foreground text-2xl">{workload.title}</h3>
<p className="text-foreground-lighter text-sm">{workload.paragraph}</p>
</div>
<ul className="flex flex-col gap-2 mt-2">
{workload.features.map((feature) => {
const Icon = feature.icon
return (
<li
key={feature.text}
className="flex items-start gap-2 text-foreground-light text-sm"
>
<Icon
className="w-4 h-4 text-brand shrink-0 mt-0.5 stroke-[1.5px]"
aria-hidden
/>
{feature.text}
</li>
)
})}
</ul>
</div>
))}
</div>
</SectionContainerWithCn>
)
}
@@ -0,0 +1,19 @@
/* Marching-dashes flow on the connector lines between sources, workloads, and
* the application. Every connector path is authored source -> target, so a
* negative offset makes the dashes travel in the direction of the flow. */
.flow {
stroke-dasharray: 3 9;
animation: flow 0.9s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -12;
}
}
@media (prefers-reduced-motion: reduce) {
.flow {
animation: none;
}
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from 'next'
import { ComputeContent } from './_components/ComputeContent'
import { mdAlternates } from '@/lib/md-alternates'
export const metadata: Metadata = {
title: 'Compute | Supabase',
description:
'Run AI-agent sandboxes and production backends on one runtime, next to your database. Now in Private Alpha.',
alternates: mdAlternates('compute'),
}
export default function ComputePage() {
return <ComputeContent />
}
+1
View File
@@ -12,6 +12,7 @@ const SEGMENT_TO_PRODUCT: Record<string, PRODUCT_NAMES> = {
auth: PRODUCT_NAMES.AUTHENTICATION,
storage: PRODUCT_NAMES.STORAGE,
'edge-functions': PRODUCT_NAMES.FUNCTIONS,
compute: PRODUCT_NAMES.COMPUTE,
realtime: PRODUCT_NAMES.REALTIME,
}
@@ -0,0 +1,178 @@
import * as Sentry from '@sentry/nextjs'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
}
const isValidEmail = (email: string): boolean => {
const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
return emailPattern.test(email)
}
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
const RATE_LIMIT_MAX = 5
const ipRequestMap = new Map<string, { count: number; resetAt: number }>()
const MAX_FIELD_LENGTH = 255
const MAX_DETAILS_LENGTH = 2000
const USE_CASE_OPTIONS = ['sandboxes', 'services', 'both']
const isFilledString = (value: unknown, maxLength: number): value is string =>
typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength
const isOptionalString = (value: unknown, maxLength: number): boolean =>
value === undefined || value === null || (typeof value === 'string' && value.length <= maxLength)
export async function OPTIONS() {
return new Response(null, {
headers: corsHeaders,
status: 204,
})
}
export async function POST(req: Request) {
const HUBSPOT_PORTAL_ID = process.env.HUBSPOT_PORTAL_ID
const HUBSPOT_FORM_GUID = process.env.HUBSPOT_COMPUTE_WAITLIST_FORM_GUID
// x-vercel-forwarded-for is set by the platform and cannot be spoofed by the client.
const ip =
req.headers.get('x-vercel-forwarded-for')?.split(',')[0]?.trim() ||
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
'unknown'
const now = Date.now()
// Drop expired entries so the map doesn't grow unbounded on a long-lived instance.
for (const [key, value] of ipRequestMap) {
if (now >= value.resetAt) ipRequestMap.delete(key)
}
const entry = ipRequestMap.get(ip)
if (entry && now < entry.resetAt) {
if (entry.count >= RATE_LIMIT_MAX) {
return new Response(JSON.stringify({ message: 'Too many requests. Try again later.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 429,
})
}
entry.count++
} else {
ipRequestMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW })
}
let body: any
try {
body = await req.json()
} catch {
return new Response(JSON.stringify({ message: 'Invalid JSON body' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
const { firstName, lastName, email, company, useCase, details, honeypot } = body
// Anti-spam honeypot: real users never fill this hidden field.
if (honeypot) {
return new Response(JSON.stringify({ message: 'Submission successful' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
}
if (
!isFilledString(firstName, MAX_FIELD_LENGTH) ||
!isFilledString(lastName, MAX_FIELD_LENGTH) ||
!isFilledString(email, MAX_FIELD_LENGTH)
) {
return new Response(JSON.stringify({ message: 'Name and email are required' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
if (
!isOptionalString(company, MAX_FIELD_LENGTH) ||
!isOptionalString(details, MAX_DETAILS_LENGTH) ||
!(
useCase === undefined ||
useCase === null ||
useCase === '' ||
USE_CASE_OPTIONS.includes(useCase)
)
) {
return new Response(JSON.stringify({ message: 'Invalid form values' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
if (!isValidEmail(email)) {
return new Response(JSON.stringify({ message: 'Invalid email address' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 422,
})
}
try {
if (!HUBSPOT_PORTAL_ID || !HUBSPOT_FORM_GUID) {
throw new Error('HubSpot credentials not configured')
}
const fields = [
{ objectTypeId: '0-1', name: 'firstname', value: firstName },
{ objectTypeId: '0-1', name: 'lastname', value: lastName },
{ objectTypeId: '0-1', name: 'email', value: email },
...(company ? [{ objectTypeId: '0-1', name: 'company', value: company }] : []),
...(useCase ? [{ objectTypeId: '0-1', name: 'compute_use_case', value: useCase }] : []),
...(details ? [{ objectTypeId: '0-1', name: 'message', value: details }] : []),
]
const response = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fields,
context: {
pageUri: 'https://supabase.com/compute',
pageName: 'Compute Private Alpha Waitlist',
},
legalConsentOptions: {
consent: {
consentToProcess: true,
text: 'By submitting this form, you acknowledge that Supabase Compute is offered as a private alpha preview, provided on an "as is" and "as available" basis without warranties of any kind. It is intended for internal evaluation only and should not be used to serve production workloads or your own end customers. Supabase may modify, suspend, or discontinue the preview at any time without notice, and there is no guarantee it will become a generally available product. You confirm you are authorized to submit this request on behalf of your organization, and that you have read and understood our Privacy Policy.',
},
},
}),
}
)
if (!response.ok) {
// Read as text: HubSpot doesn't always return JSON on errors, and a parse
// failure here would mask the real status behind the catch-all 500 below.
const errorBody = await response.text()
Sentry.captureException(new Error(`HubSpot form submission failed: ${errorBody}`))
return new Response(JSON.stringify({ message: 'Submission failed. Please try again.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: response.status,
})
}
return new Response(JSON.stringify({ message: 'Submission successful' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error: any) {
Sentry.captureException(error)
return new Response(JSON.stringify({ message: 'Submission failed. Please try again.' }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500,
})
}
}
+2 -32
View File
@@ -1,7 +1,6 @@
import { FeatureItem, type Feature } from '~/components/FeatureItem'
import SectionContainer from '~/components/Layouts/SectionContainer'
import type { LucideIcon } from 'lucide-react'
import React, { type FC, type ReactNode } from 'react'
import { cn } from 'ui'
import type { FC, ReactNode } from 'react'
interface Props {
id: string
@@ -10,12 +9,6 @@ interface Props {
features: Feature[]
}
type Feature = {
icon: LucideIcon | any
heading: string
subheading: string
}
const Support: FC<Props> = (props) => {
return (
<SectionContainer id={props.id} className="flex flex-col gap-4 md:gap-8">
@@ -32,27 +25,4 @@ const Support: FC<Props> = (props) => {
)
}
interface FeatureItemProps {
feature: Feature
}
const FeatureItem: FC<FeatureItemProps> = ({ feature }) => {
const Icon: LucideIcon = feature.icon
const iconSize = 7
const iconWidth = `w-${iconSize}`
const iconHeight = `h-${iconSize}`
return (
<li className="flex flex-col gap-2 text-sm">
<Icon className={cn('stroke-1 mb-2 text-foreground-lighter', iconWidth, iconHeight)} />
<div className="w-full h-px overflow-hidden flex items-start bg-border-muted">
<span className={cn('h-full bg-foreground-lighter', iconWidth)} />
</div>
<h4 className="text-foreground text-lg lg:text-xl mt-1">{feature.heading}</h4>
<p className="text-foreground-lighter text-sm">{feature.subheading}</p>
{/* <TextLink hasChevron label="Read story" url={feature.url} className="mt-4" /> */}
</li>
)
}
export default Support
+29
View File
@@ -0,0 +1,29 @@
import type { LucideIcon } from 'lucide-react'
import type { FC } from 'react'
export type Feature = {
icon: LucideIcon
heading: string
subheading: string
}
/**
* Icon, hairline rule, heading, subheading — the feature-list item shared by the
* Enterprise support section and the Compute highlights. The rule's leading
* segment is deliberately the same width as the icon, so keep the two `w-7`
* classes in sync if the icon size changes.
*/
export const FeatureItem: FC<{ feature: Feature }> = ({ feature }) => {
const Icon = feature.icon
return (
<li className="flex flex-col gap-2 text-sm">
<Icon className="stroke-1 mb-2 w-7 h-7 text-foreground-lighter" />
<div className="w-full h-px overflow-hidden flex items-start bg-border-muted">
<span className="h-full w-7 bg-foreground-lighter" />
</div>
<h4 className="text-foreground text-lg lg:text-xl mt-1">{feature.heading}</h4>
<p className="text-foreground-lighter text-sm">{feature.subheading}</p>
</li>
)
}
+7 -1
View File
@@ -1,7 +1,7 @@
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
import { cn } from 'ui'
import { Badge, cn } from 'ui'
const MenuItem = React.forwardRef<
React.ElementRef<'a'>,
@@ -10,6 +10,10 @@ const MenuItem = React.forwardRef<
icon?: string
hasChevron?: boolean
hoverColor?: 'foreground' | 'brand'
badge?: {
label: string
variant?: 'default' | 'warning' | 'success' | 'destructive' | 'secondary'
}
}
>(
(
@@ -22,6 +26,7 @@ const MenuItem = React.forwardRef<
hasChevron,
children,
hoverColor = 'foreground',
badge,
...props
},
ref
@@ -68,6 +73,7 @@ const MenuItem = React.forwardRef<
>
{title}
</p>
{badge && <Badge variant={badge.variant}>{badge.label}</Badge>}
{hasChevron && (
<ChevronRight
strokeWidth={2}
+5
View File
@@ -63,6 +63,11 @@ export const MobileMenu = ({ open, setOpen, menu }: Props) => {
description={component.description_short}
icon={component.icon}
onClick={() => setOpen(false)}
badge={
component.name === 'Compute'
? { label: 'Private Alpha', variant: 'default' }
: undefined
}
/>
))}
<div>
@@ -37,6 +37,11 @@ export const ProductDropdown = () => {
icon={product.icon}
className="h-fit p-0"
hasChevron
badge={
product.name === 'Compute'
? { label: 'Private Alpha', variant: 'default' }
: undefined
}
/>
</NavigationMenuLink>
))}
+44
View File
@@ -0,0 +1,44 @@
# Supabase Compute
> Run AI-agent sandboxes and production backends on one runtime, next to your database. Now in Private Alpha.
Supabase Compute is a fully managed compute service built for agentic development. Use it as a sandbox platform offering short-lived, isolated environments to execute code, or as a place to run always-on HTTP services written in any language. Everything runs in the same region and network as your primary Postgres database.
## Key Features
- **Two workload shapes**: ephemeral sandboxes for untrusted code and always-on HTTP services, on one runtime
- **Any language**: Node, Deno, or any Dockerfile in Private Alpha — no wrapper scripts or glue code
- **Next to your database**: same region and network as Postgres, with single-digit-millisecond queries
- **Scale to zero**: idle sandboxes and services suspend automatically and resume in under a second
- **No wall-clock limits**: jobs and pipelines run as long as the work takes
- **Preconfigured trust boundary**: access control via Supabase Auth with short-lived credentials; workloads reach the database and Storage with the permissions carried by their key
- **Per-workload firewalls**: define which external endpoints and ports each sandbox or service can reach
- **Cloaked secrets**: scope secrets per workload; optionally have Supabase inject values only on requests to approved services
- **Automatic patching and dependency scanning**: kernel security patches applied automatically; runtime alerts for newly disclosed vulnerabilities
- **Safe releases**: branching, progressive rollouts, automatic rollback on bad deploys, and auto-scaling
- **Built-in observability**: execution logs, traces, audit logs, and usage metrics in the Supabase dashboard
## Deployment
- Supabase CLI: `supabase compute deploy`
- MCP server: agents provision and manage sandboxes and services programmatically
- Management API: script deployments from any environment
- GitHub Actions: ready-made CI/CD workflows
- Compute skill: teach your coding agent the full surface
## Common Use Cases
- Sandboxes for AI agents executing untrusted code
- Agent frameworks and long-running agent sessions
- Transcription, embedding, and data pipelines without runtime limits
- Always-on HTTP APIs and backend services
- Background jobs and scheduled work
- MCP server hosting
## Availability
Compute is in Private Alpha behind a waitlist. Existing Edge Functions keep running unchanged; because Compute supports the Deno runtime, functions can migrate as-is when you choose.
## Links
- Product page: https://supabase.com/compute
+4
View File
@@ -26,6 +26,10 @@ const footerData = [
text: 'Functions',
url: '/edge-functions',
},
{
text: 'Compute',
url: '/compute',
},
{
text: 'Realtime',
url: '/realtime',
+13
View File
@@ -63,6 +63,19 @@ const MainProducts: ProductType = {
label: '',
url: '/edge-functions',
},
[PRODUCT_SHORTNAMES.COMPUTE]: {
name: PRODUCT_NAMES.COMPUTE,
icon: products.compute.icon[24],
description: (
<>
Run <strong>AI-agent sandboxes and backend services</strong> on one runtime, next to your
database.
</>
),
description_short: 'Sandboxes and backend services',
label: 'Private Alpha',
url: '/compute',
},
[PRODUCT_SHORTNAMES.REALTIME]: {
name: PRODUCT_NAMES.REALTIME,
icon: products.realtime.icon[24],
+1
View File
@@ -55,6 +55,7 @@
"HUBSPOT_ENTERPRISE_FORM_GUID",
"HUBSPOT_PARTNERSHIP_FORM_GUID",
"HUBSPOT_PARTNER_INTAKE_FORM_GUID",
"HUBSPOT_COMPUTE_WAITLIST_FORM_GUID",
"NOTION_FORMS_API_KEY",
"OPENAI_API_KEY",
"EMAIL_REPORT_SLACK_WEBHOOK",
+11
View File
@@ -3,6 +3,7 @@ export type PRODUCT =
| PRODUCT_SHORTNAMES.AUTHENTICATION
| PRODUCT_SHORTNAMES.STORAGE
| PRODUCT_SHORTNAMES.FUNCTIONS
| PRODUCT_SHORTNAMES.COMPUTE
| PRODUCT_SHORTNAMES.REALTIME
export enum PRODUCT_NAMES {
@@ -10,6 +11,7 @@ export enum PRODUCT_NAMES {
AUTHENTICATION = 'Authentication',
STORAGE = 'Storage',
FUNCTIONS = 'Edge Functions',
COMPUTE = 'Compute',
REALTIME = 'Realtime',
}
@@ -18,6 +20,7 @@ export enum PRODUCT_SHORTNAMES {
AUTHENTICATION = 'authentication',
STORAGE = 'storage',
FUNCTIONS = 'functions',
COMPUTE = 'compute',
REALTIME = 'realtime',
}
@@ -92,6 +95,14 @@ export const products: Products = {
'24': 'M6.6594 21.8201C8.10788 22.5739 9.75418 23 11.5 23C17.299 23 22 18.299 22 12.5C22 10.7494 21.5716 9.09889 20.8139 7.64754M16.4016 3.21191C14.9384 2.43814 13.2704 2 11.5 2C5.70101 2 1 6.70101 1 12.5C1 14.287 1.44643 15.9698 2.23384 17.4428M2.23384 17.4428C1.81058 17.96 1.55664 18.6211 1.55664 19.3416C1.55664 20.9984 2.89979 22.3416 4.55664 22.3416C6.21349 22.3416 7.55664 20.9984 7.55664 19.3416C7.55664 17.6847 6.21349 16.3416 4.55664 16.3416C3.62021 16.3416 2.78399 16.7706 2.23384 17.4428ZM21.5 5.64783C21.5 7.30468 20.1569 8.64783 18.5 8.64783C16.8432 8.64783 15.5 7.30468 15.5 5.64783C15.5 3.99097 16.8432 2.64783 18.5 2.64783C20.1569 2.64783 21.5 3.99097 21.5 5.64783ZM18.25 12.5C18.25 16.2279 15.2279 19.25 11.5 19.25C7.77208 19.25 4.75 16.2279 4.75 12.5C4.75 8.77208 7.77208 5.75 11.5 5.75C15.2279 5.75 18.25 8.77208 18.25 12.5Z',
},
},
[PRODUCT_SHORTNAMES.COMPUTE]: {
name: PRODUCT_NAMES.COMPUTE,
icon: {
'16': 'M14 10.6667V5.3333C13.9997 5.0995 13.938 4.8699 13.8211 4.6674C13.704 4.465 13.5358 4.2969 13.3333 4.18L8.6667 1.5133C8.464 1.3963 8.234 1.3347 8 1.3347C7.766 1.3347 7.536 1.3963 7.3333 1.5133L2.6667 4.18C2.4642 4.2969 2.296 4.465 2.179 4.6674C2.062 4.8699 2.0002 5.0995 2 5.3333V10.6667C2.0002 10.9004 2.062 11.1301 2.179 11.3325C2.296 11.535 2.4642 11.7031 2.6667 11.82L7.3333 14.4867C7.536 14.6036 7.766 14.6653 8 14.6653C8.234 14.6653 8.464 14.6036 8.6667 14.4867L13.3333 11.82C13.5358 11.7031 13.704 11.535 13.8211 11.3325C13.938 11.1301 13.9997 10.9004 14 10.6667Z M11.5556 9.4232V6.5781C11.5554 6.4534 11.5188 6.3309 11.4495 6.2229C11.3802 6.1149 11.2805 6.0253 11.1605 5.9629L8.3951 4.5404C8.275 4.478 8.1387 4.4451 8 4.4451C7.8613 4.4451 7.725 4.478 7.6049 4.5404L4.8395 5.9629C4.7195 6.0253 4.6198 6.1149 4.5505 6.2229C4.4812 6.3309 4.4446 6.4534 4.4444 6.5781V9.4232C4.4446 9.5479 4.4812 9.6704 4.5505 9.7784C4.6198 9.8864 4.7195 9.976 4.8395 10.0384L7.6049 11.4609C7.725 11.5234 7.8613 11.5562 8 11.5562C8.1387 11.5562 8.275 11.5234 8.3951 11.4609L11.1605 10.0384C11.2805 9.976 11.3802 9.8864 11.4495 9.7784C11.5188 9.6704 11.5554 9.5479 11.5556 9.4232Z M5.3333 6.2229L8 8.0006L10.6667 6.2229 M8 11.1111V8.0006',
'18': 'M15.75 12V5.99999C15.7497 5.73694 15.6803 5.4786 15.5487 5.25086C15.417 5.02312 15.2278 4.83401 15 4.70249L9.75 1.70249C9.52197 1.57084 9.2633 1.50153 9 1.50153C8.7367 1.50153 8.47803 1.57084 8.25 1.70249L3 4.70249C2.7722 4.83401 2.58299 5.02312 2.45135 5.25086C2.31971 5.4786 2.25027 5.73694 2.25 5.99999V12C2.25027 12.263 2.31971 12.5214 2.45135 12.7491C2.58299 12.9769 2.7722 13.166 3 13.2975L8.25 16.2975C8.47803 16.4291 8.7367 16.4985 9 16.4985C9.2633 16.4985 9.52197 16.4291 9.75 16.2975L15 13.2975C15.2278 13.166 15.417 12.9769 15.5487 12.7491C15.6803 12.5214 15.7497 12.263 15.75 12Z M13 10.6011V7.4004C12.9998 7.26009 12.9587 7.12227 12.8807 7.00079C12.8027 6.8793 12.6906 6.77842 12.5556 6.70826L9.44444 5.10793C9.30932 5.0377 9.15603 5.00073 9 5.00073C8.84397 5.00073 8.69068 5.0377 8.55556 5.10793L5.44444 6.70826C5.30945 6.77842 5.19732 6.8793 5.11932 7.00079C5.04131 7.12227 5.00016 7.26009 5 7.4004V10.6011C5.00016 10.7414 5.04131 10.8792 5.11932 11.0007C5.19732 11.1222 5.30945 11.223 5.44444 11.2932L8.55556 12.8935C8.69068 12.9638 8.84397 13.0007 9 13.0007C9.15603 13.0007 9.30932 12.9638 9.44444 12.8935L12.5556 11.2932C12.6906 11.223 12.8027 11.1222 12.8807 11.0007C12.9587 10.8792 12.9998 10.7414 13 10.6011Z M6 7.00073L9 9.00073L12 7.00073 M9 12.5V9.00073',
'24': 'M21 16V8C20.9996 7.6493 20.9071 7.3048 20.7316 7.0011C20.556 6.6975 20.3037 6.4453 20 6.27L13 2.27C12.696 2.0945 12.3511 2.002 12 2.002C11.6489 2.002 11.304 2.0945 11 2.27L4 6.27C3.6963 6.4453 3.444 6.6975 3.2685 7.0011C3.0929 7.3048 3.0004 7.6493 3 8V16C3.0004 16.3507 3.0929 16.6952 3.2685 16.9988C3.444 17.3025 3.6963 17.5547 4 17.73L11 21.73C11.304 21.9055 11.6489 21.998 12 21.998C12.3511 21.998 12.696 21.9055 13 21.73L20 17.73C20.3037 17.5547 20.556 17.3025 20.7316 16.9988C20.9071 16.6952 20.9996 16.3507 21 16Z M17.3333 14.1348V9.8672C17.3331 9.6801 17.2783 9.4964 17.1743 9.3344C17.0703 9.1724 16.9208 9.0379 16.7408 8.9443L12.5926 6.8106C12.4124 6.7169 12.208 6.6676 12 6.6676C11.792 6.6676 11.5876 6.7169 11.4074 6.8106L7.2593 8.9443C7.0793 9.0379 6.9298 9.1724 6.8258 9.3344C6.7217 9.4964 6.6669 9.6801 6.6667 9.8672V14.1348C6.6669 14.3219 6.7217 14.5056 6.8258 14.6676C6.9298 14.8296 7.0793 14.964 7.2593 15.0576L11.4074 17.1913C11.5876 17.2851 11.792 17.3343 12 17.3343C12.208 17.3343 12.4124 17.2851 12.5926 17.1913L16.7408 15.0576C16.9208 14.964 17.0703 14.8296 17.1743 14.6676C17.2783 14.5056 17.3331 14.3219 17.3333 14.1348Z M8 9.3343L12 12.001L16 9.3343 M12 16.6667V12.001',
},
},
[PRODUCT_SHORTNAMES.REALTIME]: {
name: PRODUCT_NAMES.REALTIME,
icon: {