-
+
Integrates with the Supabase ecosystem
diff --git a/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx b/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx
index df0a716187e..3e43da05a57 100644
--- a/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx
+++ b/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx
@@ -2,6 +2,7 @@
import dynamic from 'next/dynamic'
import Link from 'next/link'
+import { useState } from 'react'
import { cn } from 'ui'
const LocalDXImage = dynamic(() => import('~/components/Products/Functions/LocalDXImage'))
@@ -9,75 +10,124 @@ const ParityImage = dynamic(() => import('~/components/Products/Functions/Parity
const NpmEcosystem = dynamic(() => import('~/components/Products/Functions/NpmEcosystem'))
const CI = dynamic(() => import('~/components/Products/Functions/CI'))
-const cards = [
- {
- id: 'localDX',
- label: 'First-class local dev experience',
- paragraph:
- 'Write code with hot code reloading, a fast Language server for autocompletion, type checking and linting',
- image:
,
- imageClassname: 'pb-8 md:pb-12',
- },
- {
- id: 'parity',
- label: 'Dev and Prod parity',
- paragraph: (
- <>
- The open source{' '}
-
- Edge runtime
- {' '}
- runs locally in dev and powers functions in production
- >
- ),
- image:
,
- imageClassname: 'pb-8 md:pb-12 pt-7',
- },
- {
- id: 'ecosystem',
- label: 'Use any NPM module',
- paragraph: 'Tap into the 2+ million modules in the Deno and NPM ecosystem',
- image:
,
- },
- {
- id: 'ci',
- label: 'Continuous Integration',
- paragraph: (
- <>
- Use the{' '}
-
- Supabase CLI with GitHub actions
- {' '}
- to preview and deploy your functions along with the rest of your application
- >
- ),
- image:
,
- imageClassname: 'pl-2 pt-8 md:pt-12',
- },
-]
+function EcosystemCard() {
+ const [isHovered, setIsHovered] = useState(false)
+
+ return (
+
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+
Use any NPM module
+
+ Tap into the 2+ million modules in the Deno and NPM ecosystem
+
+
+
+ )
+}
const GRID_CLASS: Record
= {
localDX: 'col-span-1 md:border-r',
parity: 'col-span-1 xl:border-r',
- ecosystem: 'col-span-1 md:row-span-2 !border-b-0',
ci: 'col-span-1 md:col-span-2 xl:border-r !border-b-0',
}
+function ParityCard() {
+ const [isHovered, setIsHovered] = useState(false)
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
Dev and Prod parity
+
+ The open source{' '}
+
+ Edge runtime
+ {' '}
+ runs locally in dev and powers functions in production
+
+
+
+ )
+}
+
+function CICard() {
+ const [isHovered, setIsHovered] = useState(false)
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+
Continuous Integration
+
+ Use the{' '}
+
+ Supabase CLI with GitHub actions
+ {' '}
+ to preview and deploy your functions along with the rest of your application
+
+
+
+ )
+}
+
+function LocalDXCard() {
+ const [isHovered, setIsHovered] = useState(false)
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+
First-class local dev experience
+
+ Write code with hot code reloading, a fast Language server for autocompletion, type
+ checking and linting
+
+
+
+ )
+}
+
export function LocalDXSection() {
return (
{/* Header row */}
-
+
Delightful DX from
local to production
@@ -100,26 +150,11 @@ export function LocalDXSection() {
{/* Bento grid */}
-
- {cards.map((card, i) => (
-
-
- {card.image}
-
-
-
{card.label}
-
{card.paragraph}
-
-
- ))}
+
diff --git a/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx b/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx
index 54a67c65839..33fc3e1f9b1 100644
--- a/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx
+++ b/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx
@@ -59,7 +59,7 @@ export function ObservabilitySection() {
{/* Header row */}
-
+
Built-in observability
diff --git a/apps/www/components/Products/Functions/CI.tsx b/apps/www/components/Products/Functions/CI.tsx
index 4155490875a..fdcce77f888 100644
--- a/apps/www/components/Products/Functions/CI.tsx
+++ b/apps/www/components/Products/Functions/CI.tsx
@@ -1,36 +1,130 @@
-import React from 'react'
-import CodeWindow from '~/components/CodeWindow'
+'use client'
-const ciCode = `jobs:
- deploy:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- - uses: supabase/setup-cli@v1
- with:
- version: latest
- - run: supabase functions deploy
-`
+import { motion } from 'framer-motion'
+import { useEffect, useState } from 'react'
-const CI = () => (
-
-
-
-)
+const CODE_LINES = [
+ 'jobs:',
+ ' deploy:',
+ ' runs-on: ubuntu-latest',
+ '',
+ ' steps:',
+ ' - uses: actions/checkout@v4',
+ ' - uses: supabase/setup-cli@v1',
+ ' with:',
+ ' version: latest',
+ ' - run: supabase functions deploy',
+]
+
+type TokenType = 'key' | 'value' | 'punct' | 'dim' | 'plain'
+
+interface Token {
+ text: string
+ type: TokenType
+}
+
+// Colors from supabase-light / supabase-dark shiki themes
+const TOKEN_CLASS: Record
= {
+ key: '[color:#6b35dc] dark:[color:#bda4ff]',
+ value: '[color:#f1a10d] dark:[color:#ffcda1]',
+ punct: '[color:#a0a0a0] dark:[color:#ffffff99]',
+ dim: '[color:#7e7e7e]',
+ plain: '[color:#525252] dark:[color:#ffffff]',
+}
+
+function tokenizeLine(line: string): Token[] {
+ if (!line.trim()) return [{ text: '\u00a0', type: 'plain' }]
+
+ const indent = line.match(/^(\s*)/)?.[1] ?? ''
+ const trimmed = line.trimStart()
+
+ const tokens: Token[] = []
+ if (indent) tokens.push({ text: indent, type: 'plain' })
+
+ // List item: "- ..."
+ if (trimmed.startsWith('- ')) {
+ const rest = trimmed.slice(2)
+ const ci = rest.indexOf(': ')
+ tokens.push({ text: '- ', type: 'dim' })
+ if (ci >= 0) {
+ tokens.push({ text: rest.slice(0, ci), type: 'key' })
+ tokens.push({ text: ': ', type: 'punct' })
+ tokens.push({ text: rest.slice(ci + 2), type: 'value' })
+ } else {
+ tokens.push({ text: rest, type: 'value' })
+ }
+ return tokens
+ }
+
+ // "key: value"
+ const ci = trimmed.indexOf(': ')
+ if (ci >= 0) {
+ tokens.push({ text: trimmed.slice(0, ci), type: 'key' })
+ tokens.push({ text: ': ', type: 'punct' })
+ tokens.push({ text: trimmed.slice(ci + 2), type: 'value' })
+ return tokens
+ }
+
+ // "key:" only
+ if (trimmed.endsWith(':')) {
+ tokens.push({ text: trimmed.slice(0, -1), type: 'key' })
+ tokens.push({ text: ':', type: 'punct' })
+ return tokens
+ }
+
+ tokens.push({ text: trimmed, type: 'plain' })
+ return tokens
+}
+
+const containerVariants = {
+ visible: { transition: { staggerChildren: 0.08 } },
+}
+
+const lineVariants = {
+ hidden: { opacity: 0, x: -8 },
+ visible: { opacity: 1, x: 0, transition: { duration: 0.15, ease: 'easeOut' } },
+}
+
+const CI = ({ isHovered = false }: { isHovered?: boolean }) => {
+ const [hoverCount, setHoverCount] = useState(0)
+
+ useEffect(() => {
+ if (isHovered) setHoverCount((c) => c + 1)
+ }, [isHovered])
+
+ return (
+
+ {/* CodeWindow shell */}
+
+ {/* Title bar */}
+
+
+ {/* Code area */}
+
+
+ {CODE_LINES.map((line, i) => (
+
+ {tokenizeLine(line).map((token, j) => (
+
+ {token.text}
+
+ ))}
+
+ ))}
+
+
+
+
+ )
+}
export default CI
diff --git a/apps/www/components/Products/Functions/LocalDXImage.tsx b/apps/www/components/Products/Functions/LocalDXImage.tsx
index 34da459cfec..567d3a78933 100644
--- a/apps/www/components/Products/Functions/LocalDXImage.tsx
+++ b/apps/www/components/Products/Functions/LocalDXImage.tsx
@@ -1,31 +1,63 @@
+'use client'
+
+import { AnimatePresence, motion } from 'framer-motion'
import { Check, Copy } from 'lucide-react'
import Image from 'next/image'
-import { useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
-const LocalDXImage = () => {
+const COMMAND = 'supabase functions serve'
+const OUTPUT = 'Functions listening on 127.0.0.1:54321/functions/v1/'
+
+const LocalDXImage = ({ isHovered = false }: { isHovered?: boolean }) => {
const [copied, setCopied] = useState(false)
+ const [visibleChars, setVisibleChars] = useState(COMMAND.length)
+ const [showOutput, setShowOutput] = useState(false)
+ const visibleCharsRef = useRef(COMMAND.length)
+ const timeoutsRef = useRef[]>([])
+
+ const clearTimeouts = () => {
+ timeoutsRef.current.forEach(clearTimeout)
+ timeoutsRef.current = []
+ }
+
+ const setChars = (n: number) => {
+ visibleCharsRef.current = n
+ setVisibleChars(n)
+ }
+
+ useEffect(() => {
+ clearTimeouts()
+ if (isHovered) {
+ setShowOutput(false)
+ setChars(0)
+ COMMAND.split('').forEach((_, i) => {
+ const t = setTimeout(() => setChars(i + 1), i * 40)
+ timeoutsRef.current.push(t)
+ })
+ // Show output line shortly after typing completes
+ const t = setTimeout(() => setShowOutput(true), COMMAND.length * 40 + 120)
+ timeoutsRef.current.push(t)
+ } else {
+ setShowOutput(false)
+ const current = visibleCharsRef.current
+ for (let i = current; i < COMMAND.length; i++) {
+ const t = setTimeout(() => setChars(i + 1), (i - current) * 18)
+ timeoutsRef.current.push(t)
+ }
+ }
+ return clearTimeouts
+ }, [isHovered])
const handleCopy = () => {
setCopied(true)
- setTimeout(() => {
- setCopied(false)
- }, 1000)
+ setTimeout(() => setCopied(false), 1000)
}
return (
-
+
-
{
draggable={false}
className="object-cover absolute z-0 inset-0 dark:hidden block"
/>
+
- $
- supabase functions serve
-
+
$
+
+ {/* Slot: command ↔ output, same position */}
+
+ {/* Ghost text stabilises width */}
+
{COMMAND}
+
+
+ {!showOutput ? (
+
+ {COMMAND.slice(0, visibleChars)}
+ {isHovered && visibleChars < COMMAND.length && (
+
+ )}
+
+ ) : (
+
+ {OUTPUT}
+
+ )}
+
+
+
+
{copied ? (
diff --git a/apps/www/components/Products/Functions/NpmEcosystem.tsx b/apps/www/components/Products/Functions/NpmEcosystem.tsx
index d173166e3fd..43827cf1037 100644
--- a/apps/www/components/Products/Functions/NpmEcosystem.tsx
+++ b/apps/www/components/Products/Functions/NpmEcosystem.tsx
@@ -1,4 +1,6 @@
-import React from 'react'
+'use client'
+
+import { motion } from 'framer-motion'
const npmList = [
'fetch',
@@ -29,11 +31,17 @@ const npmList = [
'js-yaml',
'typescript',
'jest',
+ 'vitest',
'winston',
+ 'pino',
'debug',
'eslint',
+ 'prettier',
'nodemon',
+ 'tsx',
'dotenv',
+ 'zod',
+ 'valibot',
'@octokit/rest',
'cross-env',
'commander',
@@ -42,82 +50,167 @@ const npmList = [
'chalk',
'colors',
'ora',
+ 'kleur',
+ 'picocolors',
'@aws-sdk',
+ '@google-cloud/storage',
'axios',
+ 'got',
+ 'ky',
'passport',
'nodemailer',
+ 'resend',
+ '@sendgrid/mail',
'@supabase/auth-helpers-react',
'mongoose',
+ 'prisma',
+ 'drizzle-orm',
+ 'kysely',
'openai',
+ '@anthropic-ai/sdk',
+ 'langchain',
'jwt',
'react',
+ 'solid-js',
+ 'svelte',
'mocha',
'autoprefixer',
'@supabase/auth-ui-react',
'gray-matter',
+ 'marked',
+ 'unified',
+ 'rehype',
+ 'remark',
'request',
'prop-types',
'react-dom',
'bluebird',
+ 'p-limit',
+ 'p-queue',
'vue',
+ 'nuxt',
+ 'next',
'tslib',
+ 'ts-node',
'inquirer',
+ 'prompts',
'webpack',
+ 'esbuild',
+ 'rollup',
'classnames',
+ 'clsx',
'body-parser',
'rxjs',
+ 'xstate',
'babel-runtime',
'jquery',
- 'fetch',
- 'crypto',
- 'eslint',
- 'atob',
- 'moment',
- 'express',
- 'fastify',
- 'socket.io',
- 'async',
- 'lodash',
- 'underscore',
- 'ramda',
- 'validator',
- 'yup',
- 'day.js',
- 'date-fns',
- 'jsonwebtoken',
- 'bcrypt',
- 'minimist',
- 'chalk',
+ 'stripe',
+ '@clerk/clerk-js',
+ 'posthog-js',
+ 'sentry',
+ 'datadog-metrics',
+ 'sharp',
+ 'jimp',
+ 'pdf-lib',
+ 'archiver',
+ 'node-cron',
+ 'bull',
+ 'ioredis',
+ 'cheerio',
+ 'playwright',
+ 'puppeteer',
+ 'stytch',
+ 'twilio',
+ 'vonage',
+ '@slack/web-api',
+ 'discord.js',
+ 'telegraf',
+ 'hono',
+ 'oak',
+ 'itty-router',
+ 'zx',
+ 'execa',
+ 'nanoid',
+ 'ms',
+ 'qs',
+ 'node-fetch',
+ 'undici',
+ 'msw',
+ 'jose',
+ 'qrcode',
+ 'jsdom',
+ 'xlsx',
+ 'csv-parse',
+ 'papaparse',
+ 'yaml',
+ 'bullmq',
+ 'lru-cache',
+ 'keyv',
+ 'deepmerge',
+ 'immer',
+ 'zustand',
+ 'jotai',
]
-const NpmEcosystem = () => {
+const center = Math.floor(npmList.length / 2)
+const maxDistance = center
+
+const NpmEcosystem = ({ isHovered = false }: { isHovered?: boolean }) => {
return (
-
-
- {npmList.map((module, i) => (
-
- {module}
-
- ))}
+
+ {/* Pills */}
+
+ {npmList.map((module, i) => {
+ const distance = Math.abs(i - center)
+ return (
+
+ {module}
+
+ )
+ })}
+
+ {/* Radial gradient fade */}
-
+
+ {/* npm logo */}
+
diff --git a/apps/www/components/Products/Functions/ParityImage.tsx b/apps/www/components/Products/Functions/ParityImage.tsx
index 8cfcbb26215..913dc8ed8aa 100644
--- a/apps/www/components/Products/Functions/ParityImage.tsx
+++ b/apps/www/components/Products/Functions/ParityImage.tsx
@@ -1,7 +1,57 @@
-import Image from 'next/image'
-import React from 'react'
+'use client'
-const ParityImage = () => (
+import { motion } from 'framer-motion'
+import Image from 'next/image'
+import { cn } from 'ui'
+
+const SYNC_WIDTH = 48
+
+function SyncLines({ isHovered }: { isHovered: boolean }) {
+ const shimmerTransition = (delay: number) =>
+ isHovered
+ ? { duration: 0.7, repeat: Infinity, ease: 'linear' as const, repeatDelay: 1.8, delay }
+ : { duration: 0.4, ease: 'easeOut' as const }
+
+ const tipMask = 'linear-gradient(to right, transparent, black 28%, black 72%, transparent)'
+
+ return (
+
+ {/* → green shimmer */}
+
+
+
+ {/* ← purple shimmer */}
+
+
+
+
+ )
+}
+
+const ParityImage = ({ isHovered = false }: { isHovered?: boolean }) => (
(
draggable={false}
className="object-cover absolute z-0 inset-0 hidden dark:block"
/>
-
-
-
-
-
+ {/* Animated dashed border */}
+
+
-
+ {/* Dev badge — green border + shadow on hover */}
+
+
+
+
+ {/* Prod badge — purple border + shadow on hover */}
+