From 4efb47a6bb6f9d6d3a5dbc28cfa5cccf26ea12ed Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Wed, 1 Apr 2026 14:09:47 -0400 Subject: [PATCH] mock other product pages --- .../auth/_components/ApiSection.tsx | 280 ++++++++++ .../auth/_components/ApiSectionClient.tsx | 185 +++++++ .../auth/_components/AuthContent.tsx | 43 ++ .../auth/_components/FeaturesSection.tsx | 493 ++++++++++++++++++ .../app/(products)/auth/_components/Hero.tsx | 29 ++ .../auth/_components/HighlightsSection.tsx | 71 +++ .../auth/_components/QuoteSection.tsx | 16 + .../auth/_components/RLSSection.tsx | 117 +++++ .../auth/_components/RLSSectionClient.tsx | 163 ++++++ apps/www/app/(products)/auth/page.tsx | 15 + .../realtime/_components/ApiSection.tsx | 125 +++++ .../realtime/_components/ApiSectionClient.tsx | 163 ++++++ .../realtime/_components/FeaturesSection.tsx | 308 +++++++++++ .../(products)/realtime/_components/Hero.tsx | 29 ++ .../realtime/_components/QuoteSection.tsx | 15 + .../realtime/_components/RealtimeContent.tsx | 30 ++ apps/www/app/(products)/realtime/page.tsx | 14 + .../storage/_components/ApiSection.tsx | 213 ++++++++ .../storage/_components/ApiSectionClient.tsx | 185 +++++++ .../_components/BucketTypesSection.tsx | 81 +++ .../storage/_components/FeaturesSection.tsx | 222 ++++++++ .../(products)/storage/_components/Hero.tsx | 29 ++ .../_components/PermissionsSection.tsx | 98 ++++ .../_components/PermissionsSectionClient.tsx | 163 ++++++ .../storage/_components/QuoteSection.tsx | 15 + .../storage/_components/StorageContent.tsx | 43 ++ apps/www/app/(products)/storage/page.tsx | 15 + apps/www/pages/{auth.tsx => auth.tsx.bak} | 0 .../pages/{realtime.tsx => realtime.tsx.bak} | 0 .../pages/{storage.tsx => storage.tsx.bak} | 0 30 files changed, 3160 insertions(+) create mode 100644 apps/www/app/(products)/auth/_components/ApiSection.tsx create mode 100644 apps/www/app/(products)/auth/_components/ApiSectionClient.tsx create mode 100644 apps/www/app/(products)/auth/_components/AuthContent.tsx create mode 100644 apps/www/app/(products)/auth/_components/FeaturesSection.tsx create mode 100644 apps/www/app/(products)/auth/_components/Hero.tsx create mode 100644 apps/www/app/(products)/auth/_components/HighlightsSection.tsx create mode 100644 apps/www/app/(products)/auth/_components/QuoteSection.tsx create mode 100644 apps/www/app/(products)/auth/_components/RLSSection.tsx create mode 100644 apps/www/app/(products)/auth/_components/RLSSectionClient.tsx create mode 100644 apps/www/app/(products)/auth/page.tsx create mode 100644 apps/www/app/(products)/realtime/_components/ApiSection.tsx create mode 100644 apps/www/app/(products)/realtime/_components/ApiSectionClient.tsx create mode 100644 apps/www/app/(products)/realtime/_components/FeaturesSection.tsx create mode 100644 apps/www/app/(products)/realtime/_components/Hero.tsx create mode 100644 apps/www/app/(products)/realtime/_components/QuoteSection.tsx create mode 100644 apps/www/app/(products)/realtime/_components/RealtimeContent.tsx create mode 100644 apps/www/app/(products)/realtime/page.tsx create mode 100644 apps/www/app/(products)/storage/_components/ApiSection.tsx create mode 100644 apps/www/app/(products)/storage/_components/ApiSectionClient.tsx create mode 100644 apps/www/app/(products)/storage/_components/BucketTypesSection.tsx create mode 100644 apps/www/app/(products)/storage/_components/FeaturesSection.tsx create mode 100644 apps/www/app/(products)/storage/_components/Hero.tsx create mode 100644 apps/www/app/(products)/storage/_components/PermissionsSection.tsx create mode 100644 apps/www/app/(products)/storage/_components/PermissionsSectionClient.tsx create mode 100644 apps/www/app/(products)/storage/_components/QuoteSection.tsx create mode 100644 apps/www/app/(products)/storage/_components/StorageContent.tsx create mode 100644 apps/www/app/(products)/storage/page.tsx rename apps/www/pages/{auth.tsx => auth.tsx.bak} (100%) rename apps/www/pages/{realtime.tsx => realtime.tsx.bak} (100%) rename apps/www/pages/{storage.tsx => storage.tsx.bak} (100%) diff --git a/apps/www/app/(products)/auth/_components/ApiSection.tsx b/apps/www/app/(products)/auth/_components/ApiSection.tsx new file mode 100644 index 00000000000..3332748692f --- /dev/null +++ b/apps/www/app/(products)/auth/_components/ApiSection.tsx @@ -0,0 +1,280 @@ +import { createHighlighter, type ThemeRegistration } from 'shiki' +import { ApiSectionClient } from './ApiSectionClient' + +const supabaseDark: ThemeRegistration = { + name: 'supabase-dark', + type: 'dark', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#ffffff', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#bda4ff' } }, + { scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#3ecf8e' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#3ecf8e' } }, + { scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#3ecf8e' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['variable.parameter'], settings: { foreground: '#ffffff' } }, + { scope: ['punctuation'], settings: { foreground: '#ffffff' } }, + { scope: ['constant.numeric'], settings: { foreground: '#ededed' } }, + ], +} + +const supabaseLight: ThemeRegistration = { + name: 'supabase-light', + type: 'light', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#525252', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#6b35dc' } }, + { scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#15593b' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#15593b' } }, + { scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#15593b' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['variable.parameter'], settings: { foreground: '#525252' } }, + { scope: ['punctuation'], settings: { foreground: '#a0a0a0' } }, + { scope: ['constant.numeric'], settings: { foreground: '#525252' } }, + ], +} + +const LANG_MAP: Record = { + JavaScript: 'javascript', + Flutter: 'dart', + Python: 'python', + 'C#': 'csharp', + Kotlin: 'kotlin', + Swift: 'swift', +} + +const API_EXAMPLES = [ + { + icon: 'UserPlus' as const, + title: 'Sign up', + description: 'Create new accounts with email/password, phone, or third-party providers.', + code: { + JavaScript: `// Sign up with email and password +const { data, error } = await supabase.auth.signUp({ + email: 'user@example.com', + password: 'secure-password', +})`, + Flutter: `// Sign up with email and password +final response = await supabase.auth.signUp( + email: 'user@example.com', + password: 'secure-password', +);`, + Python: `# Sign up with email and password +response = supabase.auth.sign_up({ + "email": "user@example.com", + "password": "secure-password", +})`, + 'C#': `// Sign up with email and password +var session = await supabase.Auth.SignUp( + "user@example.com", + "secure-password" +);`, + Kotlin: `// Sign up with email and password +supabase.auth.signUpWith(Email) { + email = "user@example.com" + password = "secure-password" +}`, + Swift: `// Sign up with email and password +try await supabase.auth.signUp( + email: "user@example.com", + password: "secure-password" +)`, + }, + }, + { + icon: 'LogIn' as const, + title: 'Sign in', + description: 'Authenticate users with multiple strategies in a single call.', + code: { + JavaScript: `// Sign in with email +const { data, error } = await supabase.auth.signInWithPassword({ + email: 'user@example.com', + password: 'secure-password', +}) + +// Sign in with magic link +const { data, error } = await supabase.auth.signInWithOtp({ + email: 'user@example.com', +})`, + Flutter: `// Sign in with email +final response = await supabase.auth.signInWithPassword( + email: 'user@example.com', + password: 'secure-password', +); + +// Sign in with magic link +await supabase.auth.signInWithOtp( + email: 'user@example.com', +);`, + Python: `# Sign in with email +response = supabase.auth.sign_in_with_password({ + "email": "user@example.com", + "password": "secure-password", +}) + +# Sign in with magic link +response = supabase.auth.sign_in_with_otp({ + "email": "user@example.com", +})`, + 'C#': `// Sign in with email +var session = await supabase.Auth.SignIn( + "user@example.com", + "secure-password" +); + +// Sign in with magic link +await supabase.Auth.SignIn( + Constants.SignInType.MagicLink, + "user@example.com" +);`, + Kotlin: `// Sign in with email +supabase.auth.signInWith(Email) { + email = "user@example.com" + password = "secure-password" +} + +// Sign in with magic link +supabase.auth.signInWith(OTP) { + email = "user@example.com" +}`, + Swift: `// Sign in with email +try await supabase.auth.signIn( + email: "user@example.com", + password: "secure-password" +) + +// Sign in with magic link +try await supabase.auth.signInWithOTP( + email: "user@example.com" +)`, + }, + }, + { + icon: 'Globe' as const, + title: 'OAuth logins', + description: 'Social login with 20+ providers and custom scopes for extended permissions.', + code: { + JavaScript: `// Sign in with GitHub +const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + scopes: 'repo gist notifications', + }, +})`, + Flutter: `// Sign in with GitHub +await supabase.auth.signInWithOAuth( + OAuthProvider.github, + scopes: 'repo gist notifications', +);`, + Python: `# Sign in with GitHub +response = supabase.auth.sign_in_with_oauth({ + "provider": "github", + "options": { + "scopes": "repo gist notifications", + }, +})`, + 'C#': `// Sign in with GitHub +var session = await supabase.Auth.SignIn( + Constants.Provider.Github, + new SignInOptions { + Scopes = "repo gist notifications" + } +);`, + Kotlin: `// Sign in with GitHub +supabase.auth.signInWith(Github) { + scopes = "repo gist notifications" +}`, + Swift: `// Sign in with GitHub +try await supabase.auth.signInWithOAuth( + provider: .github, + scopes: "repo gist notifications" +)`, + }, + }, + { + icon: 'KeyRound' as const, + title: 'Session management', + description: 'Retrieve, refresh, and manage user sessions with built-in token handling.', + code: { + JavaScript: `// Get the current user +const { data: { user } } = await supabase.auth.getUser() + +// Listen for auth state changes +supabase.auth.onAuthStateChange((event, session) => { + console.log(event, session) +})`, + Flutter: `// Get the current user +final user = supabase.auth.currentUser; + +// Listen for auth state changes +supabase.auth.onAuthStateChange.listen((data) { + final event = data.event; + final session = data.session; +});`, + Python: `# Get the current user +user = supabase.auth.get_user() + +# Get the current session +session = supabase.auth.get_session()`, + 'C#': `// Get the current user +var user = supabase.Auth.CurrentUser; + +// Listen for auth state changes +supabase.Auth.AddStateChangedListener( + (sender, changed) => { + // Handle state change + } +);`, + Kotlin: `// Get the current user +val user = supabase.auth.currentUserOrNull() + +// Listen for auth state changes +supabase.auth.sessionStatus.collect { status -> + when (status) { + is SessionStatus.Authenticated -> { } + SessionStatus.NotAuthenticated -> { } + } +}`, + Swift: `// Get the current user +let user = try await supabase.auth.user() + +// Listen for auth state changes +for await (event, session) in supabase.auth.authStateChanges { + print(event, session) +}`, + }, + }, +] + +export async function ApiSection() { + const hl = await createHighlighter({ + themes: [supabaseDark, supabaseLight], + langs: Object.values(LANG_MAP), + }) + + const examples = API_EXAMPLES.map((example) => ({ + icon: example.icon, + title: example.title, + description: example.description, + languages: Object.fromEntries( + Object.entries(example.code).map(([lang, code]) => [ + lang, + { + darkHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-dark' }), + lightHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-light' }), + }, + ]) + ), + })) + + hl.dispose() + + return +} diff --git a/apps/www/app/(products)/auth/_components/ApiSectionClient.tsx b/apps/www/app/(products)/auth/_components/ApiSectionClient.tsx new file mode 100644 index 00000000000..55a7d858761 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/ApiSectionClient.tsx @@ -0,0 +1,185 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import { ChevronDown, Globe, KeyRound, LogIn, UserPlus } from 'lucide-react' +import Link from 'next/link' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' +import { cn } from 'ui' + +const ICONS = { UserPlus, LogIn, Globe, KeyRound } as const + +type IconName = keyof typeof ICONS + +type ApiExample = { + icon: IconName + title: string + description: string + languages: Record +} + +const LANGUAGES = ['JavaScript', 'Flutter', 'Python', 'C#', 'Kotlin', 'Swift'] + +const INTERVAL_DURATION = 8000 + +export function ApiSectionClient({ examples }: { examples: ApiExample[] }) { + const [activeIdx, setActiveIdx] = useState(0) + const [activeLang, setActiveLang] = useState('JavaScript') + const [progress, setProgress] = useState(0) + const intervalRef = useRef | null>(null) + const { ref: inViewRef, inView } = useInView({ threshold: 0.3 }) + const active = examples[activeIdx] + + const clearTimer = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, []) + + const startTimer = useCallback(() => { + clearTimer() + setProgress(0) + const updateFrequency = 30 + const increment = (100 / INTERVAL_DURATION) * updateFrequency + intervalRef.current = setInterval(() => { + setProgress((prev) => { + const next = prev + increment + if (next >= 100) return 100 + return next + }) + }, updateFrequency) + }, [clearTimer]) + + useEffect(() => { + if (progress >= 100) { + setActiveIdx((prev) => (prev + 1) % examples.length) + } + }, [progress, examples.length]) + + useEffect(() => { + if (inView) { + startTimer() + } else { + clearTimer() + } + return clearTimer + }, [inView, activeIdx, startTimer, clearTimer]) + + const handleTabClick = (index: number) => { + setActiveIdx(index) + startTimer() + } + + return ( +
+ {/* Header */} +
+
+

+ Simple APIs +
+ powerful libraries +

+

+ APIs that you can understand. With powerful libraries that work on client and server-side + applications. +

+
+
+ + {/* Content */} +
+
+ {/* Left: tab cards */} +
+ {examples.map((example, index) => { + const isActive = index === activeIdx + const Icon = ICONS[example.icon] + return ( + + ) + })} +
+ + {/* Right: code block */} +
+ {/* Language switcher */} +
+
+ + +
+
+ + + +
+
+ + + + Documentation + + + + +
+
+
+
+ ) +} diff --git a/apps/www/app/(products)/auth/_components/AuthContent.tsx b/apps/www/app/(products)/auth/_components/AuthContent.tsx new file mode 100644 index 00000000000..3d57624eac7 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/AuthContent.tsx @@ -0,0 +1,43 @@ +import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection' +import { CTASection } from '~/app/(home)/_components/CTASection' +import { FeaturesSection } from './FeaturesSection' +import { Hero } from './Hero' +import { HighlightsSection } from './HighlightsSection' +import { QuoteSection } from './QuoteSection' + +export function AuthContent({ + apiSlot, + rlsSlot, +}: { + apiSlot: React.ReactNode + rlsSlot: React.ReactNode +}) { + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {apiSlot} +
+
+ {rlsSlot} +
+
+ +
+
+ +
+
+ ) +} diff --git a/apps/www/app/(products)/auth/_components/FeaturesSection.tsx b/apps/www/app/(products)/auth/_components/FeaturesSection.tsx new file mode 100644 index 00000000000..c17e67a5371 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/FeaturesSection.tsx @@ -0,0 +1,493 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import { Link as LinkIcon, Shield } from 'lucide-react' +import Image from 'next/image' +import { useEffect, useRef, useState } from 'react' +import { cn } from 'ui' + +const PROVIDERS = [ + 'github', + 'google', + 'apple', + 'discord', + 'facebook', + 'gitlab', + 'microsoft', + 'twitter', + 'twitch', + 'spotify', + 'slack', + 'bitbucket', + 'twilio', + 'solana', + 'ethereum', +] + +const FEATURES = [ + { + title: 'All the social providers', + description: + 'Enable social logins with the click of a button. Google, Facebook, GitHub, Azure, and many more.', + visual: ProvidersSkeleton, + }, + { + title: 'Fully integrated', + description: + 'Built-in Authentication, Authorization, and User Management — no external services needed.', + visual: IntegratedSkeleton, + }, + { + title: 'Own your data', + description: + 'User data stored in your Supabase database. No third-party privacy concerns. Host in 16+ locations.', + visual: DataOwnershipSkeleton, + }, +] + +const COLS = 13 +const ROWS = 13 +const CELL_SIZE = 48 +const GAP = 10 + +function ProvidersSkeleton() { + const [activeCell, setActiveCell] = useState(Math.floor((COLS * ROWS) / 2)) + const prevCell = useRef(activeCell) + const containerRef = useRef(null) + const [isInView, setIsInView] = useState(false) + + const totalCells = COLS * ROWS + const grid = Array.from({ length: totalCells }, (_, i) => PROVIDERS[i % PROVIDERS.length]) + + // Keep highlight away from edges so grid always overflows the card + const MARGIN = 3 + const pickNext = () => { + const prev = prevCell.current + const prevCol = prev % COLS + const prevRow = Math.floor(prev / COLS) + let next = prev + const prevProvider = PROVIDERS[prev % PROVIDERS.length] + // Try until we get a cell 5+ tiles away and a different provider + for (let attempt = 0; attempt < 50; attempt++) { + const col = MARGIN + Math.floor(Math.random() * (COLS - MARGIN * 2)) + const row = MARGIN + Math.floor(Math.random() * (ROWS - MARGIN * 2)) + const candidate = row * COLS + col + const dist = Math.abs(col - prevCol) + Math.abs(row - prevRow) + if (dist >= 5 && PROVIDERS[candidate % PROVIDERS.length] !== prevProvider) { + next = candidate + break + } + } + prevCell.current = next + return next + } + + // Compute offset to center the active cell in the viewport + const activeCol = activeCell % COLS + const activeRow = Math.floor(activeCell / COLS) + const centerCol = Math.floor(COLS / 2) + const centerRow = Math.floor(ROWS / 2) + const offsetX = (centerCol - activeCol) * (CELL_SIZE + GAP) + const offsetY = (centerRow - activeRow) * (CELL_SIZE + GAP) + + useEffect(() => { + const el = containerRef.current + if (!el) return + const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), { + threshold: 0.3, + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!isInView) return + const timer = setInterval(() => setActiveCell(pickNext()), 5000) + return () => clearInterval(timer) + }, [isInView]) + + return ( +
+
+ {grid.map((provider, i) => { + const isActive = i === activeCell + return ( +
+ {provider} +
+ ) + })} +
+
+ ) +} + +const AUTH_STEPS = [ + { label: 'Sign up' }, + { label: 'Verify email' }, + { label: 'Create session' }, + { label: 'Access granted' }, +] + +function SignUpPreview() { + return ( +
+
Create an account
+
+
+ email@example.com +
+
+ Password +
+
+
+ Sign up +
+
+
+ or +
+
+
+
+ Google +
+
+ GitHub +
+
+
+ ) +} + +function VerifyEmailPreview() { + return ( +
+
+ + + + +
+
Check your email
+
+ We sent a verification link to +
+ email@example.com +
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+
+ ) +} + +function SessionPreview() { + return ( +
+
Session
+
+ {[ + { key: 'access_token', val: 'eyJhbG...kpXVc' }, + { key: 'token_type', val: 'bearer' }, + { key: 'expires_in', val: '3600' }, + { key: 'user.id', val: 'a1b2c3d4-...' }, + { key: 'user.email', val: 'email@example.com' }, + { key: 'user.role', val: 'authenticated' }, + ].map((row) => ( +
+ + {row.key} + + + {row.val} + +
+ ))} +
+
+ ) +} + +function AccessGrantedPreview() { + return ( +
+
+ + + +
+
Welcome back
+
You're now signed in
+
+
+
+
+
+
+
+ ) +} + +const STEP_PREVIEWS = [SignUpPreview, VerifyEmailPreview, SessionPreview, AccessGrantedPreview] + +function IntegratedSkeleton() { + const [step, setStep] = useState(0) + const containerRef = useRef(null) + const [isInView, setIsInView] = useState(false) + + useEffect(() => { + const el = containerRef.current + if (!el) return + const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), { + threshold: 0.3, + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!isInView) return + const timer = setInterval(() => setStep((s) => (s + 1) % AUTH_STEPS.length), 3000) + return () => clearInterval(timer) + }, [isInView]) + + const Preview = STEP_PREVIEWS[step] + + return ( +
+ {/* Left: steps */} +
+ {AUTH_STEPS.map((s, i) => ( +
+
+ + {s.label} + +
+ ))} +
+ + {/* Right: shared card with animated content, overflows card edges */} +
+
+ {STEP_PREVIEWS.map((StepPreview, i) => ( + + + + ))} +
+
+
+ ) +} + +function DataOwnershipSkeleton() { + const [hovered, setHovered] = useState(false) + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + maskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)', + WebkitMaskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)', + }} + > + {/* Abstract construction lines */} + + + + + + + + + + {/* Highlighted lines on hover */} + + + + + + {/* Shield icon */} +
+ +
+ RLS +
+
+
+ ) +} + +export function FeaturesSection() { + return ( +
+ {/* Header */} +
+
+

+ Everything you need +
+ for user authentication +

+

+ Social logins, email/password, magic links, phone auth, and more — with enterprise-grade + security built on PostgreSQL's Row Level Security. +

+
+
+ + {/* 3-col grid */} +
+
+ {FEATURES.map((feature) => { + const Visual = feature.visual + return ( +
+
+ +
+
+

{feature.title}

+

{feature.description}

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/auth/_components/Hero.tsx b/apps/www/app/(products)/auth/_components/Hero.tsx new file mode 100644 index 00000000000..666f6664683 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/Hero.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link' +import { Button } from 'ui' + +export function Hero() { + return ( +
+
+
+

+ Authentication + built in, not bolted on +

+
+ + +
+
+

+ Every Supabase project comes with a complete user management system. Including + PostgreSQL's Row Level Security for fine-grained access control. +

+
+
+ ) +} diff --git a/apps/www/app/(products)/auth/_components/HighlightsSection.tsx b/apps/www/app/(products)/auth/_components/HighlightsSection.tsx new file mode 100644 index 00000000000..af015bdc17f --- /dev/null +++ b/apps/www/app/(products)/auth/_components/HighlightsSection.tsx @@ -0,0 +1,71 @@ +import { Check } from 'lucide-react' +import Link from 'next/link' +import { Button } from 'ui' + +const HIGHLIGHTS = [ + { + title: 'Multi-Factor Authentication', + features: [ + 'Time-based one-time passwords (TOTP)', + 'Works with any authenticator app', + 'Enforce MFA for specific user roles', + 'Built-in enrollment and challenge flows', + ], + cta: { label: 'Learn more', href: '/docs/guides/auth/auth-mfa' }, + }, + { + title: 'Enterprise SSO', + features: [ + 'SAML 2.0 support for identity providers', + 'Connect to Azure AD, Okta, and more', + 'Automatic user provisioning on sign-in', + 'Manage SSO from the Dashboard or API', + ], + cta: { label: 'Learn more', href: '/docs/guides/auth/enterprise-sso/auth-sso-saml' }, + }, +] + +export function HighlightsSection() { + return ( +
+ {/* Header */} +
+

+ Enterprise ready +
+ security at every level +

+
+ +
+
+ {HIGHLIGHTS.map((highlight) => ( +
+
+
+

{highlight.title}

+
    + {highlight.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ +
+
+
+ ))} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/auth/_components/QuoteSection.tsx b/apps/www/app/(products)/auth/_components/QuoteSection.tsx new file mode 100644 index 00000000000..4da3b4c0094 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/QuoteSection.tsx @@ -0,0 +1,16 @@ +import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection' + +export function QuoteSection() { + return ( + + ) +} diff --git a/apps/www/app/(products)/auth/_components/RLSSection.tsx b/apps/www/app/(products)/auth/_components/RLSSection.tsx new file mode 100644 index 00000000000..e74ef03b638 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/RLSSection.tsx @@ -0,0 +1,117 @@ +import { createHighlighter, type ThemeRegistration } from 'shiki' +import { RLSSectionClient } from './RLSSectionClient' + +const supabaseDark: ThemeRegistration = { + name: 'supabase-dark', + type: 'dark', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#ffffff', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#bda4ff' } }, + { scope: ['entity.name.function', 'support.function'], settings: { foreground: '#3ecf8e' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#3ecf8e' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['punctuation'], settings: { foreground: '#ffffff' } }, + { scope: ['constant.numeric'], settings: { foreground: '#ededed' } }, + ], +} + +const supabaseLight: ThemeRegistration = { + name: 'supabase-light', + type: 'light', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#525252', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#6b35dc' } }, + { scope: ['entity.name.function', 'support.function'], settings: { foreground: '#15593b' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#15593b' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['punctuation'], settings: { foreground: '#a0a0a0' } }, + { scope: ['constant.numeric'], settings: { foreground: '#525252' } }, + ], +} + +const RLS_EXAMPLES = [ + { + title: 'Allow read access', + description: 'Public profiles are viewable by everyone.', + code: `-- 1. Create table +create table profiles ( + id serial primary key, + name text +); + +-- 2. Enable RLS +alter table profiles enable row level security; + +-- 3. Create Policy +create policy "Public profiles are viewable by everyone." +on profiles for select +using ( true );`, + }, + { + title: 'Restrict updates', + description: 'Users can only update their own rows.', + code: `-- 1. Create table +create table profiles ( + id serial primary key, + name text +); + +-- 2. Enable RLS +alter table profiles enable row level security; + +-- 3. Create Policy +create policy "Users can update their own profiles." +on profiles for update +using ( (select auth.uid()) = id );`, + }, + { + title: 'Advanced rules', + description: 'Team members can update team details using joins.', + code: `create table teams ( + id bigint primary key generated always as identity, + name text +); + +create table members ( + team_id bigint references teams, + user_id uuid references auth.users +); + +alter table teams enable row level security; + +create policy "Team members can update team details if they belong to the team" + on teams + for update using ( + (select auth.uid()) in ( + select user_id from members + where team_id = id + ) + );`, + }, +] + +export async function RLSSection() { + const hl = await createHighlighter({ + themes: [supabaseDark, supabaseLight], + langs: ['sql'], + }) + + const examples = RLS_EXAMPLES.map((example) => ({ + title: example.title, + description: example.description, + darkHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-dark' }), + lightHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-light' }), + })) + + hl.dispose() + + return +} diff --git a/apps/www/app/(products)/auth/_components/RLSSectionClient.tsx b/apps/www/app/(products)/auth/_components/RLSSectionClient.tsx new file mode 100644 index 00000000000..7ea5285df29 --- /dev/null +++ b/apps/www/app/(products)/auth/_components/RLSSectionClient.tsx @@ -0,0 +1,163 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import Link from 'next/link' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' +import { cn } from 'ui' + +type RLSExample = { + title: string + description: string + darkHtml: string + lightHtml: string +} + +const INTERVAL_DURATION = 10000 + +export function RLSSectionClient({ examples }: { examples: RLSExample[] }) { + const [activeIdx, setActiveIdx] = useState(0) + const [progress, setProgress] = useState(0) + const intervalRef = useRef | null>(null) + const { ref: inViewRef, inView } = useInView({ threshold: 0.3 }) + const active = examples[activeIdx] + + const clearTimer = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, []) + + const startTimer = useCallback(() => { + clearTimer() + setProgress(0) + const updateFrequency = 30 + const increment = (100 / INTERVAL_DURATION) * updateFrequency + intervalRef.current = setInterval(() => { + setProgress((prev) => { + const next = prev + increment + if (next >= 100) return 100 + return next + }) + }, updateFrequency) + }, [clearTimer]) + + useEffect(() => { + if (progress >= 100) { + setActiveIdx((prev) => (prev + 1) % examples.length) + } + }, [progress, examples.length]) + + useEffect(() => { + if (inView) { + startTimer() + } else { + clearTimer() + } + return clearTimer + }, [inView, activeIdx, startTimer, clearTimer]) + + const handleTabClick = (index: number) => { + setActiveIdx(index) + startTimer() + } + + return ( +
+ {/* Header */} +
+
+

+ User permissions +
+ without the middleware +

+

+ Build authorization rules with Postgres' Row Level Security — control who can + create, edit, and delete specific rows in your database. No additional servers required. +

+
+
+ + {/* Content */} +
+
+ {/* Left: tabs */} +
+ {examples.map((example, index) => { + const isActive = index === activeIdx + return ( + + ) + })} +
+ + {/* Right: code block */} +
+ + +
+
+ + + + Documentation + + + + +
+
+
+
+ ) +} diff --git a/apps/www/app/(products)/auth/page.tsx b/apps/www/app/(products)/auth/page.tsx new file mode 100644 index 00000000000..92a1d7535d6 --- /dev/null +++ b/apps/www/app/(products)/auth/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next' + +import { ApiSection } from './_components/ApiSection' +import { AuthContent } from './_components/AuthContent' +import { RLSSection } from './_components/RLSSection' + +export const metadata: Metadata = { + title: 'Auth | Supabase', + description: + 'Add user sign ups and logins to your app. Secure your data with Row Level Security. Works with all major OAuth providers.', +} + +export default function AuthPage() { + return } rlsSlot={} /> +} diff --git a/apps/www/app/(products)/realtime/_components/ApiSection.tsx b/apps/www/app/(products)/realtime/_components/ApiSection.tsx new file mode 100644 index 00000000000..f25ec279759 --- /dev/null +++ b/apps/www/app/(products)/realtime/_components/ApiSection.tsx @@ -0,0 +1,125 @@ +import { createHighlighter, type ThemeRegistration } from 'shiki' +import { ApiSectionClient } from './ApiSectionClient' + +const supabaseDark: ThemeRegistration = { + name: 'supabase-dark', + type: 'dark', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#ffffff', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#bda4ff' } }, + { scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#3ecf8e' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#3ecf8e' } }, + { scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#3ecf8e' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['variable.parameter'], settings: { foreground: '#ffffff' } }, + { scope: ['punctuation'], settings: { foreground: '#ffffff' } }, + { scope: ['constant.numeric'], settings: { foreground: '#ededed' } }, + ], +} + +const supabaseLight: ThemeRegistration = { + name: 'supabase-light', + type: 'light', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#525252', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#6b35dc' } }, + { scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#15593b' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#15593b' } }, + { scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#15593b' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['variable.parameter'], settings: { foreground: '#525252' } }, + { scope: ['punctuation'], settings: { foreground: '#a0a0a0' } }, + { scope: ['constant.numeric'], settings: { foreground: '#525252' } }, + ], +} + +const API_EXAMPLES = [ + { + icon: 'Database' as const, + title: 'Database changes', + description: 'Listen to inserts, updates, and deletes in real time.', + code: `import { createClient } from '@supabase/supabase-js' + +const supabase = createClient('URL', 'ANON_KEY') +const channel = supabase + .channel('db-changes') + .on('postgres_changes', { + event: 'INSERT', + schema: 'public', + table: 'messages', + }, (payload) => { + console.log('New message:', payload.new) + }) + .subscribe()`, + }, + { + icon: 'Users' as const, + title: 'Presence', + description: 'Track and synchronize online user state across clients.', + code: `import { createClient } from '@supabase/supabase-js' + +const supabase = createClient('URL', 'ANON_KEY') +const channel = supabase.channel('online-users') + +channel + .on('presence', { event: 'sync' }, () => { + const state = channel.presenceState() + console.log('Online users:', state) + }) + .subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + await channel.track({ user: 'user-123' }) + } + })`, + }, + { + icon: 'Radio' as const, + title: 'Broadcast', + description: 'Send ephemeral messages to all subscribed clients instantly.', + code: `import { createClient } from '@supabase/supabase-js' + +const supabase = createClient('URL', 'ANON_KEY') +const channel = supabase.channel('cursor-pos') + +channel + .on('broadcast', { event: 'cursor' }, (payload) => { + console.log('Cursor position:', payload) + }) + .subscribe(async (status) => { + if (status === 'SUBSCRIBED') { + await channel.send({ + type: 'broadcast', + event: 'cursor', + payload: { x: 100, y: 200 }, + }) + } + })`, + }, +] + +export async function ApiSection() { + const hl = await createHighlighter({ + themes: [supabaseDark, supabaseLight], + langs: ['javascript'], + }) + + const examples = API_EXAMPLES.map((example) => ({ + icon: example.icon, + title: example.title, + description: example.description, + darkHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-dark' }), + lightHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-light' }), + })) + + hl.dispose() + + return +} diff --git a/apps/www/app/(products)/realtime/_components/ApiSectionClient.tsx b/apps/www/app/(products)/realtime/_components/ApiSectionClient.tsx new file mode 100644 index 00000000000..1a859d7d147 --- /dev/null +++ b/apps/www/app/(products)/realtime/_components/ApiSectionClient.tsx @@ -0,0 +1,163 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import { Database, Radio, Users } from 'lucide-react' +import Link from 'next/link' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' +import { cn } from 'ui' + +const ICONS = { Database, Users, Radio } as const + +type IconName = keyof typeof ICONS + +type ApiExample = { + icon: IconName + title: string + description: string + darkHtml: string + lightHtml: string +} + +const INTERVAL_DURATION = 10000 + +export function ApiSectionClient({ examples }: { examples: ApiExample[] }) { + const [activeIdx, setActiveIdx] = useState(0) + const [progress, setProgress] = useState(0) + const intervalRef = useRef | null>(null) + const { ref: inViewRef, inView } = useInView({ threshold: 0.3 }) + const active = examples[activeIdx] + + const clearTimer = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, []) + + const startTimer = useCallback(() => { + clearTimer() + setProgress(0) + const updateFrequency = 30 + const increment = (100 / INTERVAL_DURATION) * updateFrequency + intervalRef.current = setInterval(() => { + setProgress((prev) => { + const next = prev + increment + if (next >= 100) return 100 + return next + }) + }, updateFrequency) + }, [clearTimer]) + + useEffect(() => { + if (progress >= 100) { + setActiveIdx((prev) => (prev + 1) % examples.length) + } + }, [progress, examples.length]) + + useEffect(() => { + if (inView) { + startTimer() + } else { + clearTimer() + } + return clearTimer + }, [inView, activeIdx, startTimer, clearTimer]) + + const handleTabClick = (index: number) => { + setActiveIdx(index) + startTimer() + } + + return ( +
+ {/* Header */} +
+
+

+ Simple and convenient +
+ APIs +

+

+ APIs that you can understand. With powerful libraries that work on client and server-side + applications. +

+
+
+ + {/* Content */} +
+
+ {/* Left: tab cards */} +
+ {examples.map((example, index) => { + const isActive = index === activeIdx + const Icon = ICONS[example.icon] + return ( + + ) + })} +
+ + {/* Right: code block */} +
+ + +
+
+ + + + Documentation + + + + +
+
+
+
+ ) +} diff --git a/apps/www/app/(products)/realtime/_components/FeaturesSection.tsx b/apps/www/app/(products)/realtime/_components/FeaturesSection.tsx new file mode 100644 index 00000000000..3964bd93453 --- /dev/null +++ b/apps/www/app/(products)/realtime/_components/FeaturesSection.tsx @@ -0,0 +1,308 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import { useEffect, useRef, useState } from 'react' +import { cn } from 'ui' + +const FEATURES = [ + { + title: 'Database changes', + description: + 'Listen to inserts, updates, and deletes in your Postgres database and react instantly.', + visual: DatabaseChangesSkeleton, + }, + { + title: 'Presence', + description: + 'Store and synchronize online user state consistently across all connected clients.', + visual: PresenceSkeleton, + }, + { + title: 'Broadcast', + description: 'Send any data to any client subscribed to the same channel with low latency.', + visual: BroadcastSkeleton, + }, +] + +type ChangeEvent = { op: string; table: string; row: string } + +const DB_EVENTS: ChangeEvent[] = [ + { op: 'INSERT', table: 'messages', row: "{ id: 42, text: 'Hello!' }" }, + { op: 'UPDATE', table: 'users', row: "{ id: 7, status: 'online' }" }, + { op: 'DELETE', table: 'messages', row: '{ id: 38 }' }, + { op: 'INSERT', table: 'orders', row: "{ id: 99, total: 49.00 }" }, + { op: 'UPDATE', table: 'messages', row: "{ id: 42, text: 'Updated' }" }, +] + +function DatabaseChangesSkeleton() { + const [eventIdx, setEventIdx] = useState(0) + const containerRef = useRef(null) + const [isInView, setIsInView] = useState(false) + + useEffect(() => { + const el = containerRef.current + if (!el) return + const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), { + threshold: 0.3, + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!isInView) return + const timer = setInterval(() => setEventIdx((i) => (i + 1) % DB_EVENTS.length), 2500) + return () => clearInterval(timer) + }, [isInView]) + + const event = DB_EVENTS[eventIdx] + + return ( +
+
+
+          event
+          {': '}
+          
+            
+              {event.op}
+            
+          
+          {'\n'}
+          table
+          {': '}
+          
+            
+              {event.table}
+            
+          
+          {'\n'}
+          new
+          {': '}
+          
+            
+              {event.row}
+            
+          
+        
+
+
+ ) +} + +const USERS = [ + { name: 'Alice', color: '#3ECF8E' }, + { name: 'Bob', color: '#6c63ff' }, + { name: 'Carol', color: '#F06A50' }, + { name: 'Dave', color: '#ffcda1' }, +] + +function PresenceSkeleton() { + const [onlineCount, setOnlineCount] = useState(2) + const containerRef = useRef(null) + const [isInView, setIsInView] = useState(false) + + useEffect(() => { + const el = containerRef.current + if (!el) return + const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), { + threshold: 0.3, + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!isInView) return + const timer = setInterval(() => { + setOnlineCount((c) => { + if (c >= USERS.length) return 1 + return c + 1 + }) + }, 2000) + return () => clearInterval(timer) + }, [isInView]) + + return ( +
+
+ {USERS.map((user, i) => { + const isOnline = i < onlineCount + return ( +
+
+ + {user.name} + + + {isOnline ? 'online' : 'offline'} + +
+ ) + })} +
+
+ ) +} + +function BroadcastSkeleton() { + const [flash, setFlash] = useState(false) + const containerRef = useRef(null) + const [isInView, setIsInView] = useState(false) + + useEffect(() => { + const el = containerRef.current + if (!el) return + const observer = new IntersectionObserver(([entry]) => setIsInView(entry.isIntersecting), { + threshold: 0.3, + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!isInView) return + const timer = setInterval(() => { + setFlash(true) + setTimeout(() => setFlash(false), 600) + }, 2000) + return () => clearInterval(timer) + }, [isInView]) + + const clients = ['Client A', 'Client B', 'Client C'] + + return ( +
+
+ {/* Channel hub */} +
+ channel:room-1 +
+ + {/* Lines to clients */} +
+ {clients.map((client) => ( +
+
+
+ {client} +
+
+ ))} +
+
+
+ ) +} + +export function FeaturesSection() { + return ( +
+ {/* Header */} +
+
+

+ Three ways +
+ to go realtime +

+

+ Database changes, presence tracking, and broadcast messaging — everything you need to + build collaborative, real-time applications. +

+
+
+ + {/* 3-col grid */} +
+
+ {FEATURES.map((feature) => { + const Visual = feature.visual + return ( +
+
+ +
+
+

{feature.title}

+

{feature.description}

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/realtime/_components/Hero.tsx b/apps/www/app/(products)/realtime/_components/Hero.tsx new file mode 100644 index 00000000000..11bb8eb22c3 --- /dev/null +++ b/apps/www/app/(products)/realtime/_components/Hero.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link' +import { Button } from 'ui' + +export function Hero() { + return ( +
+
+
+

+ Realtime + sync in milliseconds +

+
+ + +
+
+

+ Sync client state globally over WebSockets. Listen to database changes, store user + presence, and broadcast messages to all connected clients. +

+
+
+ ) +} diff --git a/apps/www/app/(products)/realtime/_components/QuoteSection.tsx b/apps/www/app/(products)/realtime/_components/QuoteSection.tsx new file mode 100644 index 00000000000..d0a5b86328b --- /dev/null +++ b/apps/www/app/(products)/realtime/_components/QuoteSection.tsx @@ -0,0 +1,15 @@ +import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection' + +export function QuoteSection() { + return ( + + ) +} diff --git a/apps/www/app/(products)/realtime/_components/RealtimeContent.tsx b/apps/www/app/(products)/realtime/_components/RealtimeContent.tsx new file mode 100644 index 00000000000..d08bf91643a --- /dev/null +++ b/apps/www/app/(products)/realtime/_components/RealtimeContent.tsx @@ -0,0 +1,30 @@ +import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection' +import { CTASection } from '~/app/(home)/_components/CTASection' +import { FeaturesSection } from './FeaturesSection' +import { Hero } from './Hero' +import { QuoteSection } from './QuoteSection' + +export function RealtimeContent({ apiSlot }: { apiSlot: React.ReactNode }) { + return ( +
+
+ +
+
+ +
+
+ +
+
+ {apiSlot} +
+
+ +
+
+ +
+
+ ) +} diff --git a/apps/www/app/(products)/realtime/page.tsx b/apps/www/app/(products)/realtime/page.tsx new file mode 100644 index 00000000000..d9018b99747 --- /dev/null +++ b/apps/www/app/(products)/realtime/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next' + +import { ApiSection } from './_components/ApiSection' +import { RealtimeContent } from './_components/RealtimeContent' + +export const metadata: Metadata = { + title: 'Realtime | Supabase', + description: + 'Sync client state globally over WebSockets. Listen to database changes, store user presence, and broadcast messages in real time.', +} + +export default function RealtimePage() { + return } /> +} diff --git a/apps/www/app/(products)/storage/_components/ApiSection.tsx b/apps/www/app/(products)/storage/_components/ApiSection.tsx new file mode 100644 index 00000000000..8630c501320 --- /dev/null +++ b/apps/www/app/(products)/storage/_components/ApiSection.tsx @@ -0,0 +1,213 @@ +import { createHighlighter, type ThemeRegistration } from 'shiki' +import { ApiSectionClient } from './ApiSectionClient' + +const supabaseDark: ThemeRegistration = { + name: 'supabase-dark', + type: 'dark', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#ffffff', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#bda4ff' } }, + { scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#3ecf8e' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#3ecf8e' } }, + { scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#3ecf8e' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['variable.parameter'], settings: { foreground: '#ffffff' } }, + { scope: ['punctuation'], settings: { foreground: '#ffffff' } }, + { scope: ['constant.numeric'], settings: { foreground: '#ededed' } }, + ], +} + +const supabaseLight: ThemeRegistration = { + name: 'supabase-light', + type: 'light', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#525252', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#6b35dc' } }, + { scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#15593b' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#15593b' } }, + { scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#15593b' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['variable.parameter'], settings: { foreground: '#525252' } }, + { scope: ['punctuation'], settings: { foreground: '#a0a0a0' } }, + { scope: ['constant.numeric'], settings: { foreground: '#525252' } }, + ], +} + +const LANG_MAP: Record = { + JavaScript: 'javascript', + Flutter: 'dart', + Python: 'python', + 'C#': 'csharp', + Kotlin: 'kotlin', + Swift: 'swift', +} + +const API_EXAMPLES = [ + { + icon: 'Upload' as const, + title: 'Upload a file', + description: 'Upload any file type to your storage buckets with a single call.', + code: { + JavaScript: `// Upload an image to the "avatars" bucket +const spaceCat = event.target.files[0] +const { data, error } = await supabase + .storage + .from('avatars') + .upload('space-cat.png', spaceCat)`, + Flutter: `// Upload an image to the "avatars" bucket +final file = File('space-cat.png'); +await supabase + .storage + .from('avatars') + .upload('space-cat.png', file);`, + Python: `# Upload an image to the "avatars" bucket +with open("space-cat.png", "rb") as f: + supabase.storage.from_("avatars").upload( + "space-cat.png", f + )`, + 'C#': `// Upload an image to the "avatars" bucket +var bytes = File.ReadAllBytes("space-cat.png"); +await supabase.Storage + .From("avatars") + .Upload(bytes, "space-cat.png");`, + Kotlin: `// Upload an image to the "avatars" bucket +supabase.storage.from("avatars") + .upload("space-cat.png", file)`, + Swift: `// Upload an image to the "avatars" bucket +let data = try Data(contentsOf: fileURL) +try await supabase.storage + .from("avatars") + .upload("space-cat.png", data: data)`, + }, + }, + { + icon: 'Download' as const, + title: 'Download a file', + description: 'Retrieve files from your buckets directly or via signed URLs.', + code: { + JavaScript: `// Download the "space-cat.png" from "avatars" +const { data, error } = await supabase + .storage + .from('avatars') + .download('space-cat.png')`, + Flutter: `// Download the "space-cat.png" from "avatars" +final bytes = await supabase + .storage + .from('avatars') + .download('space-cat.png');`, + Python: `# Download the "space-cat.png" from "avatars" +data = supabase.storage.from_("avatars") \\ + .download("space-cat.png")`, + 'C#': `// Download the "space-cat.png" from "avatars" +var bytes = await supabase.Storage + .From("avatars") + .Download("space-cat.png");`, + Kotlin: `// Download the "space-cat.png" from "avatars" +val bytes = supabase.storage.from("avatars") + .downloadAuthenticated("space-cat.png")`, + Swift: `// Download the "space-cat.png" from "avatars" +let data = try await supabase.storage + .from("avatars") + .download(path: "space-cat.png")`, + }, + }, + { + icon: 'List' as const, + title: 'List files', + description: 'Browse and list all files within a storage bucket.', + code: { + JavaScript: `// List all the files in the "avatars" bucket +const { data, error } = await supabase + .storage + .from('avatars') + .list()`, + Flutter: `// List all the files in the "avatars" bucket +final files = await supabase + .storage + .from('avatars') + .list();`, + Python: `# List all the files in the "avatars" bucket +files = supabase.storage.from_("avatars") \\ + .list()`, + 'C#': `// List all the files in the "avatars" bucket +var files = await supabase.Storage + .From("avatars") + .List();`, + Kotlin: `// List all the files in the "avatars" bucket +val files = supabase.storage.from("avatars") + .list()`, + Swift: `// List all the files in the "avatars" bucket +let files = try await supabase.storage + .from("avatars") + .list()`, + }, + }, + { + icon: 'ArrowRightLeft' as const, + title: 'Move and rename', + description: 'Organize your files by moving and renaming within or across buckets.', + code: { + JavaScript: `// Move and rename files +const { data, error } = await supabase + .storage + .from('avatars') + .move('public/space-cat.png', 'private/space-cat.png')`, + Flutter: `// Move and rename files +await supabase + .storage + .from('avatars') + .move('public/space-cat.png', 'private/space-cat.png');`, + Python: `# Move and rename files +supabase.storage.from_("avatars") \\ + .move("public/space-cat.png", "private/space-cat.png")`, + 'C#': `// Move and rename files +await supabase.Storage + .From("avatars") + .Move("public/space-cat.png", "private/space-cat.png");`, + Kotlin: `// Move and rename files +supabase.storage.from("avatars") + .move("public/space-cat.png", "private/space-cat.png")`, + Swift: `// Move and rename files +try await supabase.storage + .from("avatars") + .move( + from: "public/space-cat.png", + to: "private/space-cat.png" + )`, + }, + }, +] + +export async function ApiSection() { + const hl = await createHighlighter({ + themes: [supabaseDark, supabaseLight], + langs: Object.values(LANG_MAP), + }) + + const examples = API_EXAMPLES.map((example) => ({ + icon: example.icon, + title: example.title, + description: example.description, + languages: Object.fromEntries( + Object.entries(example.code).map(([lang, code]) => [ + lang, + { + darkHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-dark' }), + lightHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-light' }), + }, + ]) + ), + })) + + hl.dispose() + + return +} diff --git a/apps/www/app/(products)/storage/_components/ApiSectionClient.tsx b/apps/www/app/(products)/storage/_components/ApiSectionClient.tsx new file mode 100644 index 00000000000..40551cbb71f --- /dev/null +++ b/apps/www/app/(products)/storage/_components/ApiSectionClient.tsx @@ -0,0 +1,185 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import { ArrowRightLeft, ChevronDown, Download, List, Upload } from 'lucide-react' +import Link from 'next/link' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' +import { cn } from 'ui' + +const ICONS = { Upload, Download, List, ArrowRightLeft } as const + +type IconName = keyof typeof ICONS + +type ApiExample = { + icon: IconName + title: string + description: string + languages: Record +} + +const LANGUAGES = ['JavaScript', 'Flutter', 'Python', 'C#', 'Kotlin', 'Swift'] + +const INTERVAL_DURATION = 8000 + +export function ApiSectionClient({ examples }: { examples: ApiExample[] }) { + const [activeIdx, setActiveIdx] = useState(0) + const [activeLang, setActiveLang] = useState('JavaScript') + const [progress, setProgress] = useState(0) + const intervalRef = useRef | null>(null) + const { ref: inViewRef, inView } = useInView({ threshold: 0.3 }) + const active = examples[activeIdx] + + const clearTimer = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, []) + + const startTimer = useCallback(() => { + clearTimer() + setProgress(0) + const updateFrequency = 30 + const increment = (100 / INTERVAL_DURATION) * updateFrequency + intervalRef.current = setInterval(() => { + setProgress((prev) => { + const next = prev + increment + if (next >= 100) return 100 + return next + }) + }, updateFrequency) + }, [clearTimer]) + + useEffect(() => { + if (progress >= 100) { + setActiveIdx((prev) => (prev + 1) % examples.length) + } + }, [progress, examples.length]) + + useEffect(() => { + if (inView) { + startTimer() + } else { + clearTimer() + } + return clearTimer + }, [inView, activeIdx, startTimer, clearTimer]) + + const handleTabClick = (index: number) => { + setActiveIdx(index) + startTimer() + } + + return ( +
+ {/* Header */} +
+
+

+ Simple and convenient +
+ APIs +

+

+ Built from the ground up for interoperable authentication. Fast and easy to implement + using our powerful library clients. +

+
+
+ + {/* Content */} +
+
+ {/* Left: tab cards */} +
+ {examples.map((example, index) => { + const isActive = index === activeIdx + const Icon = ICONS[example.icon] + return ( + + ) + })} +
+ + {/* Right: code block */} +
+ {/* Language switcher */} +
+
+ + +
+
+ + + +
+
+ + + + Documentation + + + + +
+
+
+
+ ) +} diff --git a/apps/www/app/(products)/storage/_components/BucketTypesSection.tsx b/apps/www/app/(products)/storage/_components/BucketTypesSection.tsx new file mode 100644 index 00000000000..488550878f0 --- /dev/null +++ b/apps/www/app/(products)/storage/_components/BucketTypesSection.tsx @@ -0,0 +1,81 @@ +import { Check } from 'lucide-react' +import Link from 'next/link' +import { Button } from 'ui' + +const BUCKET_TYPES = [ + { + title: 'Files buckets', + features: [ + 'Store images, videos, documents, and more', + 'Serve fast from a global CDN', + 'Fine-grained access controls', + 'Image optimizations and transformations', + ], + cta: { label: 'Learn more', href: '/docs/guides/storage' }, + }, + { + title: 'Analytics buckets', + features: [ + 'Apache Iceberg open table format', + 'Efficient query, partition, and transform', + 'Historical and time-series data', + 'Optionally expose via Postgres', + ], + cta: { label: 'Learn more', href: '/docs/guides/storage/analytics-buckets' }, + }, + { + title: 'Vector buckets', + features: [ + 'Store and index vector embeddings', + 'Multiple distance metrics', + 'Metadata filtering and similarity queries', + 'RAG systems and AI-powered search', + ], + cta: { label: 'Learn more', href: '/docs/guides/storage/vector-buckets' }, + }, +] + +export function BucketTypesSection() { + return ( +
+ {/* Header */} +
+

+ Bucket types +
+ for every application +

+
+ +
+
+ {BUCKET_TYPES.map((bucket) => ( +
+
+
+

{bucket.title}

+
    + {bucket.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ +
+
+
+ ))} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/storage/_components/FeaturesSection.tsx b/apps/www/app/(products)/storage/_components/FeaturesSection.tsx new file mode 100644 index 00000000000..a0d303c0e21 --- /dev/null +++ b/apps/www/app/(products)/storage/_components/FeaturesSection.tsx @@ -0,0 +1,222 @@ +'use client' + +import { useState } from 'react' +import { cn } from 'ui' + +const FEATURES = [ + { + title: 'Interoperable', + description: + 'Integrates with the rest of Supabase — Auth and Postgres work together seamlessly.', + visual: InteroperableSkeleton, + }, + { + title: 'Lightning fast', + description: + 'Thin API server layer that leverages Postgres permissions and delivers content via global CDN.', + visual: CDNSkeleton, + }, + { + title: 'Multiple bucket types', + description: + 'Files, Analytics, or Vector buckets — choose the right storage model for your application.', + visual: BucketsSkeleton, + }, +] + +function InteroperableSkeleton() { + const [hovered, setHovered] = useState(false) + + const products = [ + { name: 'Storage', color: '#3ECF8E' }, + { name: 'Auth', color: '#6c63ff' }, + { name: 'Database', color: '#3ECF8E' }, + ] + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + maskImage: 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)', + WebkitMaskImage: + 'radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 75%)', + }} + > +
+ {products.map((product, i) => ( +
+
+ + {product.name} + +
+ {i < products.length - 1 && ( +
+ )} +
+ ))} +
+
+ ) +} + +function CDNSkeleton() { + return ( +
+ + {/* Globe outline */} + + + + + {/* CDN nodes */} + {[ + { cx: 160, cy: 60 }, + { cx: 260, cy: 130 }, + { cx: 240, cy: 230 }, + { cx: 80, cy: 230 }, + { cx: 60, cy: 130 }, + ].map((pos, i) => ( + + + + + + ))} + {/* Center node */} + + + +
+ ) +} + +function BucketsSkeleton() { + const buckets = [ + { name: 'Files', label: 'images, videos, docs' }, + { name: 'Analytics', label: 'Iceberg, time-series' }, + { name: 'Vectors', label: 'embeddings, RAG' }, + ] + + return ( +
+
+ {buckets.map((bucket) => ( +
+ {bucket.name} + {bucket.label} +
+ ))} +
+
+ ) +} + +export function FeaturesSection() { + return ( +
+ {/* Header */} +
+
+

+ Everything you need +
+ for object storage +

+

+ S3 compatible object storage with global CDN, image transformations, and fine-grained + access controls powered by Postgres RLS. +

+
+
+ + {/* 3-col grid */} +
+
+ {FEATURES.map((feature) => { + const Visual = feature.visual + return ( +
+
+ +
+
+

{feature.title}

+

{feature.description}

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/apps/www/app/(products)/storage/_components/Hero.tsx b/apps/www/app/(products)/storage/_components/Hero.tsx new file mode 100644 index 00000000000..4f84cce19a8 --- /dev/null +++ b/apps/www/app/(products)/storage/_components/Hero.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link' +import { Button } from 'ui' + +export function Hero() { + return ( +
+
+
+

+ Store and serve + any type of digital content +

+
+ + +
+
+

+ An open source S3 Compatible Object Store with unlimited scalability, for any file type. + With custom policies and permissions that are familiar and easy to implement. +

+
+
+ ) +} diff --git a/apps/www/app/(products)/storage/_components/PermissionsSection.tsx b/apps/www/app/(products)/storage/_components/PermissionsSection.tsx new file mode 100644 index 00000000000..792d427e707 --- /dev/null +++ b/apps/www/app/(products)/storage/_components/PermissionsSection.tsx @@ -0,0 +1,98 @@ +import { createHighlighter, type ThemeRegistration } from 'shiki' +import { PermissionsSectionClient } from './PermissionsSectionClient' + +const supabaseDark: ThemeRegistration = { + name: 'supabase-dark', + type: 'dark', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#ffffff', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#bda4ff' } }, + { scope: ['entity.name.function', 'support.function'], settings: { foreground: '#3ecf8e' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#3ecf8e' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['punctuation'], settings: { foreground: '#ffffff' } }, + { scope: ['constant.numeric'], settings: { foreground: '#ededed' } }, + ], +} + +const supabaseLight: ThemeRegistration = { + name: 'supabase-light', + type: 'light', + colors: { + 'editor.background': '#00000000', + 'editor.foreground': '#525252', + }, + tokenColors: [ + { scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#6b35dc' } }, + { scope: ['entity.name.function', 'support.function'], settings: { foreground: '#15593b' } }, + { scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#15593b' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } }, + { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } }, + { scope: ['punctuation'], settings: { foreground: '#a0a0a0' } }, + { scope: ['constant.numeric'], settings: { foreground: '#525252' } }, + ], +} + +const PERMISSION_EXAMPLES = [ + { + title: 'Public access to a bucket', + description: 'Allow any user access to all objects in a bucket.', + code: `create policy "Public Access" +on storage.objects for all +using ( bucket_id = 'avatars' );`, + }, + { + title: 'Public access to a folder', + description: "Allow public CRUD access to a specific folder within a bucket.", + code: `create policy "Public access to a folder" +on storage.objects for all +using ( + bucket_id = 'avatars' + and (storage.foldername(name))[1] = 'public' +);`, + }, + { + title: 'Authenticated access', + description: 'Restrict access to authenticated users for a specific folder.', + code: `create policy "Logged in access" +on storage.objects +for all using ( + bucket_id = 'avatars' + and auth.role() = 'authenticated' + and (storage.foldername(name))[1] = 'authenticated' +);`, + }, + { + title: 'Individual file access', + description: 'Grant a specific user access to a single file by UID.', + code: `create policy "Individual access" +on storage.objects for all +using ( + bucket_id = 'avatars' + and name = 'folder/only_uid.jpg' + and (select auth.uid()) = 'd8c7bce9-cfeb-497b-bd61-e66ce2cbdaa2' +);`, + }, +] + +export async function PermissionsSection() { + const hl = await createHighlighter({ + themes: [supabaseDark, supabaseLight], + langs: ['sql'], + }) + + const examples = PERMISSION_EXAMPLES.map((example) => ({ + title: example.title, + description: example.description, + darkHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-dark' }), + lightHtml: hl.codeToHtml(example.code, { lang: 'sql', theme: 'supabase-light' }), + })) + + hl.dispose() + + return +} diff --git a/apps/www/app/(products)/storage/_components/PermissionsSectionClient.tsx b/apps/www/app/(products)/storage/_components/PermissionsSectionClient.tsx new file mode 100644 index 00000000000..3e9c993c74f --- /dev/null +++ b/apps/www/app/(products)/storage/_components/PermissionsSectionClient.tsx @@ -0,0 +1,163 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' +import Link from 'next/link' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' +import { cn } from 'ui' + +type PermissionExample = { + title: string + description: string + darkHtml: string + lightHtml: string +} + +const INTERVAL_DURATION = 10000 + +export function PermissionsSectionClient({ examples }: { examples: PermissionExample[] }) { + const [activeIdx, setActiveIdx] = useState(0) + const [progress, setProgress] = useState(0) + const intervalRef = useRef | null>(null) + const { ref: inViewRef, inView } = useInView({ threshold: 0.3 }) + const active = examples[activeIdx] + + const clearTimer = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + }, []) + + const startTimer = useCallback(() => { + clearTimer() + setProgress(0) + const updateFrequency = 30 + const increment = (100 / INTERVAL_DURATION) * updateFrequency + intervalRef.current = setInterval(() => { + setProgress((prev) => { + const next = prev + increment + if (next >= 100) return 100 + return next + }) + }, updateFrequency) + }, [clearTimer]) + + useEffect(() => { + if (progress >= 100) { + setActiveIdx((prev) => (prev + 1) % examples.length) + } + }, [progress, examples.length]) + + useEffect(() => { + if (inView) { + startTimer() + } else { + clearTimer() + } + return clearTimer + }, [inView, activeIdx, startTimer, clearTimer]) + + const handleTabClick = (index: number) => { + setActiveIdx(index) + startTimer() + } + + return ( +
+ {/* Header */} +
+
+

+ Integrates natively +
+ with Supabase Auth +

+

+ Storage authorization is built around Postgres so that you can use any combination of + SQL, Postgres functions, and even your own metadata to write policies. +

+
+
+ + {/* Content */} +
+
+ {/* Left: tabs */} +
+ {examples.map((example, index) => { + const isActive = index === activeIdx + return ( + + ) + })} +
+ + {/* Right: code block */} +
+ + +
+
+ + + + Documentation + + + + +
+
+
+
+ ) +} diff --git a/apps/www/app/(products)/storage/_components/QuoteSection.tsx b/apps/www/app/(products)/storage/_components/QuoteSection.tsx new file mode 100644 index 00000000000..8b6858ee5ab --- /dev/null +++ b/apps/www/app/(products)/storage/_components/QuoteSection.tsx @@ -0,0 +1,15 @@ +import { QuoteSection as SharedQuoteSection } from '../../_shared/QuoteSection' + +export function QuoteSection() { + return ( + + ) +} diff --git a/apps/www/app/(products)/storage/_components/StorageContent.tsx b/apps/www/app/(products)/storage/_components/StorageContent.tsx new file mode 100644 index 00000000000..b1253fd7366 --- /dev/null +++ b/apps/www/app/(products)/storage/_components/StorageContent.tsx @@ -0,0 +1,43 @@ +import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection' +import { CTASection } from '~/app/(home)/_components/CTASection' +import { BucketTypesSection } from './BucketTypesSection' +import { FeaturesSection } from './FeaturesSection' +import { Hero } from './Hero' +import { QuoteSection } from './QuoteSection' + +export function StorageContent({ + apiSlot, + permissionsSlot, +}: { + apiSlot: React.ReactNode + permissionsSlot: React.ReactNode +}) { + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {apiSlot} +
+
+ {permissionsSlot} +
+
+ +
+
+ +
+
+ ) +} diff --git a/apps/www/app/(products)/storage/page.tsx b/apps/www/app/(products)/storage/page.tsx new file mode 100644 index 00000000000..769f08fe471 --- /dev/null +++ b/apps/www/app/(products)/storage/page.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from 'next' + +import { ApiSection } from './_components/ApiSection' +import { PermissionsSection } from './_components/PermissionsSection' +import { StorageContent } from './_components/StorageContent' + +export const metadata: Metadata = { + title: 'Storage | Supabase', + description: + 'An open source S3 Compatible Object Store with unlimited scalability, for any file type.', +} + +export default function StoragePage() { + return } permissionsSlot={} /> +} diff --git a/apps/www/pages/auth.tsx b/apps/www/pages/auth.tsx.bak similarity index 100% rename from apps/www/pages/auth.tsx rename to apps/www/pages/auth.tsx.bak diff --git a/apps/www/pages/realtime.tsx b/apps/www/pages/realtime.tsx.bak similarity index 100% rename from apps/www/pages/realtime.tsx rename to apps/www/pages/realtime.tsx.bak diff --git a/apps/www/pages/storage.tsx b/apps/www/pages/storage.tsx.bak similarity index 100% rename from apps/www/pages/storage.tsx rename to apps/www/pages/storage.tsx.bak