+ 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.
+
+ {/* 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.
+
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/apps/www/app/(products)/compute/_components/WorkloadsSection.tsx b/apps/www/app/(products)/compute/_components/WorkloadsSection.tsx
new file mode 100644
index 00000000000..7dd9211b1fc
--- /dev/null
+++ b/apps/www/app/(products)/compute/_components/WorkloadsSection.tsx
@@ -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 (
+
+
+
+ Workloads
+
+
+
+ One runtime,
+ two shapes of work
+
+
+ Sandboxes, APIs, and background jobs usually mean three vendors and a database somewhere
+ else. Compute runs them all in Supabase alongside your application backend.
+
+
+ )
+}
diff --git a/apps/www/app/(products)/compute/_components/compute-visual.module.css b/apps/www/app/(products)/compute/_components/compute-visual.module.css
new file mode 100644
index 00000000000..ca25f24cb34
--- /dev/null
+++ b/apps/www/app/(products)/compute/_components/compute-visual.module.css
@@ -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;
+ }
+}
diff --git a/apps/www/app/(products)/compute/page.tsx b/apps/www/app/(products)/compute/page.tsx
new file mode 100644
index 00000000000..b2953776ea0
--- /dev/null
+++ b/apps/www/app/(products)/compute/page.tsx
@@ -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
+}
diff --git a/apps/www/app/(products)/layout.tsx b/apps/www/app/(products)/layout.tsx
index df95193d971..0dc4894ce67 100644
--- a/apps/www/app/(products)/layout.tsx
+++ b/apps/www/app/(products)/layout.tsx
@@ -12,6 +12,7 @@ const SEGMENT_TO_PRODUCT: Record = {
auth: PRODUCT_NAMES.AUTHENTICATION,
storage: PRODUCT_NAMES.STORAGE,
'edge-functions': PRODUCT_NAMES.FUNCTIONS,
+ compute: PRODUCT_NAMES.COMPUTE,
realtime: PRODUCT_NAMES.REALTIME,
}
diff --git a/apps/www/app/api-v2/submit-form-compute-waitlist/route.tsx b/apps/www/app/api-v2/submit-form-compute-waitlist/route.tsx
new file mode 100644
index 00000000000..b7bd004a0cb
--- /dev/null
+++ b/apps/www/app/api-v2/submit-form-compute-waitlist/route.tsx
@@ -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()
+
+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,
+ })
+ }
+}
diff --git a/apps/www/components/Enterprise/Support.tsx b/apps/www/components/Enterprise/Support.tsx
index cbbfdb19fa0..2949a7a2682 100644
--- a/apps/www/components/Enterprise/Support.tsx
+++ b/apps/www/components/Enterprise/Support.tsx
@@ -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) => {
return (
@@ -32,27 +25,4 @@ const Support: FC = (props) => {
)
}
-interface FeatureItemProps {
- feature: Feature
-}
-
-const FeatureItem: FC = ({ feature }) => {
- const Icon: LucideIcon = feature.icon
- const iconSize = 7
- const iconWidth = `w-${iconSize}`
- const iconHeight = `h-${iconSize}`
-
- return (
-
-
-
-
-
-
{feature.heading}
-
{feature.subheading}
- {/* */}
-
- )
-}
-
export default Support
diff --git a/apps/www/components/FeatureItem.tsx b/apps/www/components/FeatureItem.tsx
new file mode 100644
index 00000000000..db53347490e
--- /dev/null
+++ b/apps/www/components/FeatureItem.tsx
@@ -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 (
+