diff --git a/.claude/skills/telemetry-standards/SKILL.md b/.claude/skills/telemetry-standards/SKILL.md new file mode 100644 index 00000000000..0ed2f7f85bc --- /dev/null +++ b/.claude/skills/telemetry-standards/SKILL.md @@ -0,0 +1,170 @@ +--- +name: telemetry-standards +description: PostHog event tracking standards for Supabase Studio. Use when reviewing + PRs for telemetry compliance or implementing new event tracking. Covers event naming, + property conventions, approved patterns, and implementation guide. +--- + +# Telemetry Standards for Supabase Studio + +Standards for PostHog event tracking in `apps/studio/`. Apply these when +reviewing PRs that touch tracking or when implementing new tracking. + +## Event Naming + +**Format:** `[object]_[verb]` in snake_case + +**Approved verbs only:** +opened, clicked, submitted, created, removed, updated, retrieved, intended, evaluated, added, +enabled, disabled, copied, exposed, failed, converted + +**Flag these:** +- Unapproved verbs (saved, viewed, seen, pressed, etc.) +- Wrong order: `click_product_card` → should be `product_card_clicked` +- Wrong casing: `productCardClicked` → should be `product_card_clicked` + +**Good examples:** +- `product_card_clicked` +- `backup_button_clicked` +- `sql_query_submitted` + +**Common mistakes with corrections:** +- `database_saved` → `save_button_clicked` or `database_updated` (unapproved verb) +- `click_backup_button` → `backup_button_clicked` (wrong order) +- `dashboardViewed` → don't track passive views on page load +- `component_rendered` → don't track — no user interaction + +## Property Standards + +**Casing:** camelCase preferred for new events. The codebase has existing snake_case properties (e.g., `schema_name`, `table_name`) — when adding properties to an existing event, match its established convention. + +**Names must be self-explanatory:** +- `{ productType: 'database', planTier: 'pro' }` +- `{ assistantType: 'sql', suggestionType: 'optimization' }` + +**Flag these:** +- Generic names: `label`, `value`, `name`, `data` +- PascalCase properties +- Inconsistent names across similar events (e.g., `assistantType` in one event, `aiType` in a related event) +- Mixing camelCase and snake_case within the same event + +## What NOT to Track + +- Passive views/renders on page load (`dashboard_viewed`, `sidebar_appeared`, `page_loaded`) +- Component appearances without user interaction +- Generic "viewed" or "seen" events — already captured by pageview events + +**DO track:** user clicks, form submissions, explicit opens/closes, user-initiated actions. + +**Exception:** `_exposed` events for A/B experiment exposure tracking are valid even though they fire on render. + +**Never track PII** (emails, names, IPs, etc.) in event properties. + +## Required Pattern + +Import `useTrack` from `lib/telemetry/track` (within `apps/studio/`). Never use `useSendEventMutation` (deprecated). + +```typescript +import { useTrack } from 'lib/telemetry/track' + +const MyComponent = () => { + const track = useTrack() + + const handleClick = () => { + track('product_card_clicked', { + productType: 'database', + planTier: 'pro', + source: 'dashboard', + }) + } + + return +} +``` + +## Event Definitions + +All events must be defined as TypeScript interfaces in `packages/common/telemetry-constants.ts`: + +```typescript +/** + * [Event description] + * + * @group Events + * @source [what triggers this event] + */ +export interface MyFeatureClickedEvent { + action: 'my_feature_clicked' + properties: { + /** Description of property */ + featureType: string + } + groups: TelemetryGroups +} +``` + +Add the new interface to the `TelemetryEvent` union type so `useTrack` picks it up. +`@group Events` and `@source` must be accurate. + +## Review Rules + +When reviewing a PR, flag these as **required changes:** + +1. **Naming violations** — event not following `[object]_[verb]` snake_case, or using an unapproved verb +2. **Property violations** — not camelCase, generic names, or inconsistent with similar events +3. **Deprecated hook** — any usage of `useSendEventMutation` instead of `useTrack` +4. **Unnecessary view tracking** — events that fire on page load without user interaction +5. **Inaccurate docs** — `@page`/`@source` descriptions that don't match the actual implementation + +When a PR adds user-facing interactions (buttons, forms, toggles, modals) **without** tracking, suggest: +- "This adds a user interaction that may benefit from tracking." +- Propose the event name following `[object]_[verb]` convention +- Propose the `useTrack()` call with suggested properties + +When checking property consistency, search `packages/common/telemetry-constants.ts` for similar events and verify property names match. + +## Well-Formed Event Examples + +From the actual codebase: + +```typescript +// User copies a connection string +track('connection_string_copied', { + connectionType: 'psql', + connectionMethod: 'transaction_pooler', + connectionTab: 'Connection String', +}) + +// User enables a feature preview +track('feature_preview_enabled', { + feature: 'realtime_inspector', +}) + +// User clicks a banner CTA +track('index_advisor_banner_dismiss_button_clicked') + +// Experiment exposure (fires on render — valid exception) +track('home_new_experiment_exposed', { + variant: 'treatment', +}) +``` + +## Implementing New Tracking + +To add tracking for a user action: + +1. **Name the event** — `[object]_[verb]` using approved verbs only +2. **Choose properties** — camelCase preferred for new events; check `packages/common/telemetry-constants.ts` for similar events and match their property names and casing +3. **Add interface to telemetry-constants.ts** — with `@group Events` and `@source` JSDoc, add to the `TelemetryEvent` union type +4. **Add to component** — `import { useTrack } from 'lib/telemetry/track'`, call `track('event_name', { properties })` + +### Verification checklist + +- [ ] Event name follows `[object]_[verb]` with approved verb +- [ ] Event name is snake_case +- [ ] Properties are camelCase and self-explanatory +- [ ] Event defined in telemetry-constants.ts with accurate `@page`/`@source` +- [ ] Using `useTrack` hook (not `useSendEventMutation`) +- [ ] Not tracking passive views/appearances +- [ ] No PII in event properties (emails, names, IPs, etc.) +- [ ] Property names consistent with similar events diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index e7f0cbb39b7..c2a31de86b0 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -15,8 +15,16 @@ permissions: jobs: test: + name: 'E2E tests' timeout-minutes: 60 runs-on: blacksmith-4vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2] + shardTotal: [2] + outputs: + tests_ran: ${{ steps.filter.outputs.studio == 'true' }} env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -55,13 +63,65 @@ jobs: if: steps.filter.outputs.studio == 'true' run: pnpm -C e2e/studio exec playwright install chromium --with-deps --only-shell + - name: Set up NextJS/Turbo cache + if: steps.filter.outputs.studio == 'true' + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + # See here for caching with `yarn`, `bun` or other package managers https://github.com/actions/cache/blob/main/examples.md or you can leverage caching with actions/setup-node https://github.com/actions/setup-node + path: | + .turbo/cache + apps/studio/.next/build + apps/studio/.next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('apps/studio/**/*.js', 'apps/studio/**/*.jsx', 'apps/studio/**/*.ts', 'apps/studio/**/*.tsx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}- + - name: 🚀 Run Playwright tests against Vercel Preview if: steps.filter.outputs.studio == 'true' id: playwright - run: pnpm e2e + run: pnpm e2e --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + + - name: Upload blob report to GitHub Actions Artifacts + if: always() && steps.filter.outputs.studio == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: blob-report-${{ matrix.shardIndex }} + path: e2e/studio/blob-report + retention-days: 7 + + - name: Fail job if tests failed + if: steps.filter.outputs.studio == 'true' && steps.playwright.outcome != 'success' + run: | + echo "E2E tests failed" >&2 + exit 1 + + merge-reports: + name: 'E2E reports' + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() && needs.test.outputs.tests_ran == 'true' }} + needs: [test] + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Use Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v 5.0.0 + with: + path: e2e/studio/blob-report + pattern: blob-report-* + merge-multiple: true + + - name: Merge Playwright reports + run: npx playwright merge-reports --config=e2e/studio/playwright.merge.config.ts -- e2e/studio/blob-report - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: always() && steps.filter.outputs.studio == 'true' with: name: playwright-artifacts path: | @@ -70,14 +130,21 @@ jobs: retention-days: 7 - name: Comment Playwright test results on PR + if: always() && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork uses: daun/playwright-report-comment@be9e270edd5ad86038604d3caa84a819a6ff6fed # v3.10.0 - if: always() && steps.filter.outputs.studio == 'true' && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork with: report-file: e2e/studio/test-results/test-results.json comment-title: '🎭 Playwright Test Results' - - name: Fail job if tests failed - if: steps.filter.outputs.studio == 'true' && (steps.playwright.outcome != 'success' || steps.summarize.outputs.flaky_count > 0) - run: | - echo "E2E tests failed" >&2 - exit 1 + merge-results: + name: 'E2E results' + runs-on: ubuntu-latest + needs: [test] + if: ${{ !cancelled() && needs.test.outputs.tests_ran == 'true' }} + steps: + - name: All tests ok + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: exit 0 + - name: Some tests failed + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 diff --git a/.github/workflows/studio-unit-tests.yml b/.github/workflows/studio-unit-tests.yml index 3c8db2064e9..f7e96718d5e 100644 --- a/.github/workflows/studio-unit-tests.yml +++ b/.github/workflows/studio-unit-tests.yml @@ -79,6 +79,10 @@ jobs: continue-on-error: true runs-on: blacksmith-4vcpu-ubuntu-2404 steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + sparse-checkout: | + apps/studio - name: Download coverage artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: diff --git a/.gitignore b/.gitignore index a2566899919..6f13c135c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -156,4 +156,7 @@ gcloud.json keys.json # Playwright MCP -.playwright-mcp/* \ No newline at end of file +.playwright-mcp/* + +# Application +**/.superset/** diff --git a/.prettierignore b/.prettierignore index e65459c8c02..e0cec2dda80 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ .expo .next node_modules -package-lock.json +pnpm-lock.yaml docker* apps/**/out # prettier-plugin-sql-cst only supports sqlite syntax diff --git a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx index f4e4c6694d9..f3cdccc24d5 100644 --- a/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx +++ b/apps/design-system/app/(app)/docs/[[...slug]]/page.tsx @@ -1,5 +1,5 @@ import { Mdx } from '@/components/mdx-components' -import { DocsPager } from '@/components/pager' +import { DocsPager, getBreadcrumbSegments } from '@/components/pager' import { SourcePanel } from '@/components/source-panel' import { DashboardTableOfContents } from '@/components/toc' import { siteConfig } from '@/config/site' @@ -12,6 +12,7 @@ import '@/styles/mdx.css' import { allDocs } from 'contentlayer/generated' import { ChevronRight } from 'lucide-react' import type { Metadata } from 'next' +import Link from 'next/link' import { notFound } from 'next/navigation' import Balancer from 'react-wrap-balancer' import { ScrollArea, Separator } from 'ui' @@ -83,15 +84,39 @@ export default async function DocPage(props: DocPageProps) { } const toc = await getTableOfContents(doc.body.raw) + const breadcrumbSegments = getBreadcrumbSegments(doc) return (
Sign in via magic link with your email below
- -{props.children},
Price,
}
diff --git a/apps/docs/features/ui/guide/Guide.tsx b/apps/docs/features/ui/guide/Guide.tsx
index e0179dd7ab3..9fa9b8c31c3 100644
--- a/apps/docs/features/ui/guide/Guide.tsx
+++ b/apps/docs/features/ui/guide/Guide.tsx
@@ -3,7 +3,7 @@
import { createContext, useContext, type ReactNode } from 'react'
import { cn } from 'ui'
-import GuidesTableOfContents from '~/components/GuidesTableOfContents'
+import GuidesTableOfContents from '~/components/GuidesSidebar'
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
import { type GuideFrontmatter } from '~/lib/docs'
diff --git a/apps/docs/internals/generate-guides-markdown.ts b/apps/docs/internals/generate-guides-markdown.ts
new file mode 100644
index 00000000000..c07cb27bcb2
--- /dev/null
+++ b/apps/docs/internals/generate-guides-markdown.ts
@@ -0,0 +1,165 @@
+import fs from 'fs'
+import path from 'path'
+import { globby } from 'globby'
+import matter from 'gray-matter'
+
+const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials')
+
+/**
+ * Reads <$Partial path="..." /> tags and replaces them with the file contents.
+ * Recurses to handle nested partials.
+ */
+async function inlinePartials(content: string): PromiseFor secret keys, see API settings.
diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx index e2e91db0874..9ccd9c8d47d 100644 --- a/apps/studio/components/interfaces/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Connect/Connect.tsx @@ -5,7 +5,6 @@ import { DatabaseConnectionString } from 'components/interfaces/Connect/Database import { McpTabContent } from 'components/interfaces/Connect/McpTabContent' import Panel from 'components/ui/Panel' import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query' -import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { BASE_PATH } from 'lib/constants' @@ -35,6 +34,7 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C import { getContentFilePath, inferConnectTabFromParentKey } from './Connect.utils' import { ConnectDropdown } from './ConnectDropdown' import { ConnectTabContent } from './ConnectTabContent' +import { useProjectApiUrl } from '@/data/config/project-endpoint-query' export const Connect = () => { const router = useRouter() @@ -97,7 +97,8 @@ export const Connect = () => { ?.children.find((child) => child.key === selectedChild)?.children[0]?.key || '' ) - const { data: settings } = useProjectSettingsV2Query({ projectRef }, { enabled: open }) + const { data: resolvedEndpoint } = useProjectApiUrl({ projectRef }) + const { can: canReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.READ, 'service_api_keys' @@ -216,22 +217,12 @@ export const Connect = () => { : { anonKey: null, publishableKey: null } const projectKeys = useMemo(() => { - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint ?? '' - const apiHost = canReadAPIKeys ? `${protocol}://${endpoint ?? '-'}` : '' - return { - apiUrl: apiHost ?? null, + apiUrl: resolvedEndpoint ?? null, anonKey: anonKey?.api_key ?? null, publishableKey: publishableKey?.api_key ?? null, } - }, [ - settings?.app_config?.protocol, - settings?.app_config?.endpoint, - canReadAPIKeys, - anonKey?.api_key, - publishableKey?.api_key, - ]) + }, [resolvedEndpoint, anonKey?.api_key, publishableKey?.api_key]) const filePath = getContentFilePath({ connectionObject, diff --git a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx index 954ad513a77..b760a5d613f 100644 --- a/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectTabContent.tsx @@ -6,7 +6,7 @@ import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements' import { pluckObjectFields } from 'lib/helpers' import { useTrack } from 'lib/telemetry/track' import { cn, CopyCallbackContext } from 'ui' @@ -34,8 +34,7 @@ export const ConnectTabContent = forwardRefError: {(error as any)?.message ?? 'Unknown'}
+Error: {error?.message ?? 'Unknown'}
> } > diff --git a/apps/studio/components/interfaces/Database/Replication/ComingSoon.tsx b/apps/studio/components/interfaces/Database/Replication/ComingSoon.tsx deleted file mode 100644 index 0816a5cd1fe..00000000000 --- a/apps/studio/components/interfaces/Database/Replication/ComingSoon.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { ArrowRight, ArrowUpRight, Circle, Database, Plus } from 'lucide-react' -import { useTheme } from 'next-themes' -import Link from 'next/link' -import { useMemo } from 'react' -import ReactFlow, { Background, Handle, Position, ReactFlowProvider } from 'reactflow' - -import 'reactflow/dist/style.css' - -import { useParams } from 'common' -import { BASE_PATH } from 'lib/constants' -import { Button, Card, CardContent } from 'ui' - -import { NODE_WIDTH } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' - -const STATIC_NODES = [ - { - id: '1', - type: 'primary', - data: { - label: 'Primary Database', - region: 'East US (Ohio)', - provider: 'AWS', - regionIcon: 'us-east-1', - }, - position: { x: 825, y: 0 }, - }, - { - id: '2', - type: 'replica', - data: { - label: 'Iceberg', - details: '3 tables', - regionIcon: 'us-west-1', - }, - position: { x: 875, y: 110 }, - }, - { - id: '3', - type: 'replica', - data: { - label: 'BigQuery', - details: '5 tables', - regionIcon: 'us-west-1', - }, - position: { x: 875, y: 200 }, - }, - { - id: '4', - type: 'blank', - position: { x: 875, y: 290 }, - data: {}, - }, - { - id: '5', - type: 'cta', - position: { x: 125, y: 20 }, - data: {}, - }, -] - -const STATIC_EDGES = [ - { id: 'e1-2', source: '1', target: '2', type: 'smoothstep', animated: true }, - { id: 'e1-3', source: '1', target: '3', type: 'smoothstep', animated: true }, - { id: 'e1-4', source: '1', target: '4', type: 'smoothstep', animated: true }, -] - -export const ReplicationComingSoon = () => { - return ( -{data.label}
-- {data.region} -
-- {data.provider} -
-{data.label}
-- {data.details} -
-- Automatically replicate your data to external data warehouses and analytics platforms in - real-time. No manual exports, no lag. -
-- We are currently in private alpha and slowly - onboarding new customers to ensure stable data pipelines. Request access below to join the - waitlist. Read replicas are available now. -
-- Maximum time the pipeline waits to collect additional changes before - flushing a batch. + Maximum time pipeline waits to collect additional changes before flushing a + batch.
- Lower values reduce replication latency; higher values improve batching + Lower values reduce replication latency, higher values improve batching efficiency.
> @@ -122,7 +126,7 @@ export const AdvancedSettings = ({ Number of parallel connections each table copy can use during initial sync.- Higher values can copy large tables faster, but consume more database + More connections speed up large table copies, but use more database connections.
> @@ -143,47 +147,118 @@ export const AdvancedSettings = ({ )} /> - {/* BigQuery-specific: Max staleness */} +Error
++ Blocks startup for manual recovery +
+Recreate
++ Rebuilds the slot and restarts replication from scratch +
+- Maximum allowed age for BigQuery cached metadata before reading base - tables. -
-- Lower values improve freshness; higher values can reduce query cost and - latency. -
- > - } - > -Size of the BigQuery Storage Write API connection pool.
++ More connections allow more parallel writes, but consume more resources. +
+ > + } + > ++ Maximum allowed age for BigQuery cached metadata before reading base + tables. +
++ Lower values improve freshness, higher values can reduce query cost and + latency. +
+ > + } + > +BigQuery
- {unifiedReplication &&Send data to Google Cloud's data warehouse for analytics and business intelligence @@ -109,7 +108,7 @@ export const DestinationTypeSelection = () => {
Analytics Bucket
- {unifiedReplication &&Send data to Apache Iceberg tables in your Supabase Storage for flexible analytics @@ -121,7 +120,7 @@ export const DestinationTypeSelection = () => { )} - {destinationType !== 'Read Replica' && enablePgReplicate && ( + {destinationType !== 'Read Replica' && (
Replication is in alpha. Expect rapid changes and possible breaking updates.{' '}
stripe
+ {dbLine} stripe
stripe{' '}
- schema for synced Stripe data
+ {tableAndViewLine} stripe schema for
+ synced Stripe data
- Updating JWT secret: {jwtSecretUpdateProgressMessage} -
-Generate a new JWT secret
-- A random secret will be created, or you can create your own. -
-Generate a random secret
-Create my own secret
-JWT signing keys are coming soon
-- We're rolling out JWT signing keys to better support your application needs. -
-- Upgrade to a Team or Enterprise Plan to send your logs to your preferred platform + Upgrade to a Pro, Team or Enterprise Plan to send your logs to your preferred platform
+ {/* This should only be shown to free tier users so upgrade to Pro makes sense */}- Share with us why you're downgrading your plan. + What made you decide to downgrade your plan?
+ {app.name} +
++
{app.app_id}
{app.name}
-+
{app.client_id}
Published Apps
-- Build integrations that extend Supabase's functionality -
-You do not have any published applications yet
-Authorized Apps
-- Applications that have access to your organization's settings and projects -
+- You do not have any authorized applications yet -
-Delivery ID
+{selectedDelivery.id}
+ Attempt
++ {deliveryAttempt ? `#${deliveryAttempt}` : '-'} +
+Timestamp
+Response code
+ {selectedDelivery.responseCode ? ( +Event type
+{selectedDelivery.eventType}
+ Event ID
+{selectedDelivery.id}
+ Event timestamp
+Payload
+
+ {deliveryEventPayload}
+
+ Status
+Response code
+ {selectedDelivery.responseCode ? ( +Response payload
+
+ {deliveryResponsePayload}
+
+
+ {eventType}
+
+ ))}
+ {header.key}:
+ {header.value}
+ {filteredDeliveries.length} total
++ {group.label} +
+ {selectedInGroup.length > 0 && ( + + {selectedInGroup.length} + + )} +Select the region closest to your users for the best performance.
- {showNonProdFields && ( -Only these regions are supported for local/staging projects:
-Select the region closest to your users for the best performance.
+ {showNonProdFields && ( +Only these regions are supported for local/staging projects:
+Compute Size
@@ -358,7 +360,7 @@ export const Addons = () => { <>Dedicated IPv4 address
@@ -409,16 +411,26 @@ export const Addons = () => {Point in time recovery
@@ -528,7 +540,7 @@ export const Addons = () => { }, }} > - Change point in time recovery + Enable point in time recovery ) : (Custom domain
@@ -608,19 +620,28 @@ export const Addons = () => { ? 'Custom domain is enabled' : 'Custom domain is not enabled'} -$0
+/ month
+ > + ), + priceRowClassName: 'mt-2', + }, + ...availableOptions.map((option) => ({ + value: option.identifier, + id: option.identifier, + title: 'Dedicated IPv4 address', + description: 'Allow database connections from IPv4 networks.', + priceContent: ( + <> ++ {formatCurrency(option.price)} +
+/ month / database
+ > + ), + priceRowClassName: 'mt-3', + })), + ] useEffect(() => { if (visible) { @@ -112,26 +140,22 @@ const IPv4SidePanel = () => { : undefined } header={ -- Direct connections to the database only work if your client is able to resolve IPv6 - addresses. Enabling the dedicated IPv4 add-on allows you to directly connect to your - database via a IPv4 address. + Your project’s direct connection endpoint and dedicated pooler are IPv6-only by default. + Enable the dedicated IPv4 address add-on to connect from IPv4-only networks. +
+ ++ The shared pooler endpoint accepts IPv4 connections by default and does not require this + add-on.
{!isAws && ( @@ -141,84 +165,42 @@ const IPv4SidePanel = () => { /> )} - {isPgBouncerEnabled ? ( -
- If you are connecting via the Shared connection pooler, you do not need this add-on as
- our pooler resolves to IPv4 addresses. You can check your connection info in your{' '}
-
No IPv4 address
-- Use connection pooler or IPv6 for direct connections -
-$0
-/ month
-Dedicated IPv4 address
-- Allow direct database connections via IPv4 address -
-- {formatCurrency(option.price)} -
-- / month / database -
+ {ipv4Options.map((option) => ( +{option.title}
+{option.description}
+
- By default, this is only applied to the Primary database for your project. If{' '}
-
- Read replicas
- {' '}
+ By default, this is only applied to the primary database for your project. If{' '}
+
- There are no immediate charges. The addon is billed at the end of your billing cycle - based on your usage and prorated to the hour. + There are no immediate charges. The add-on is billed at the end of your billing + cycle based on your usage and prorated to the hour.
> )} {!hasAccessToIPv4 && (Upgrade your plan to enable a IPv4 address for your project
+Upgrade your plan to enable an IPv4 address for your project
- If your network only supports IPv4, consider purchasing the{' '}
-
If you are intending to use more than 500MB of disk space, then you will need to
upgrade to at least the Pro Plan.
@@ -192,11 +196,11 @@ Read more about [disk management](${DOCS_URL}/guides/platform/database-size#disk
If you are intending to use more than 500MB of disk space, then you will need to
upgrade to at least the Pro Plan.
@@ -205,11 +210,11 @@ const DiskSizeConfigurationModal = ({
- {region.name}
- {region.region}
-
- New replica will cost an additional{' '}
-
- {formatCurrency(
- estComputeMonthlyCost +
- additionalCostDiskSize +
- Number(additionalCostIOPS) +
- Number(additionalCostThroughput)
- )}
- /month
-
-
- Read replicas will match the compute size of your primary database and will
- include 25% more disk size than the primary database to accommodate WAL files.
-
- The additional cost for the replica breaks down to:
-
- Read replicas will be on the same compute size as your primary database. Deploying a
- read replica on the{' '}
- {selectedComputeMeta?.name} size incurs
- additional{' '}
-
- {selectedComputeMeta?.price_description}
-
- .
-
- Read more about{' '}
-
- billing
- {' '}
- for read replicas.
-
- Help us improve by sharing why you're deleting your project.
-
+ What made you decide to delete your project?
- )}
- >
-
-
-
Setting up your organization...
+ > + ) : isConfirmed ? ( + <> ++ Your Supabase organization has been linked successfully. +
+You can close this window.
+ > + ) : isPending ? ( ++ Stripe Product wants to create a Supabase organization for{' '} + {accountRequest.email} + {accountRequest.name && <> ({accountRequest.name})>}. +
+ {(accountRequest as any).email_matches ? ( ++ This will create a new Supabase organization linked to your Stripe account. +
+ > + ) : isError ? ( + <> +{error?.message}
+ > + ) : null} +- {unifiedReplication - ? 'Deploy read replicas across multiple regions, or replicate database changes to external data warehouses and analytics platforms' - : 'Automatically replicate your database changes to external data warehouses and analytics platforms in real-time'} -
-+ Deploy read replicas across multiple regions, or replicate database changes to + external data warehouses and analytics platforms +
++ Our AI agents are only as good as the data they can reach. Supabase gave them access to our entire + CRM, stored what they produced, and never got in the way. It is the quiet part of our stack that + makes everything else work. ++ +[Brevo](https://www.brevo.com) is a Paris-based Omnichannel Customer Engagement platform used by over 600,000 businesses worldwide. Email marketing, SMS, WhatsApp, CRM, marketing automation. Brevo surpassed $218M in ARR in 2025 and hit unicorn status with a $1B valuation to take on HubSpot and Salesforce. + +## A CRM full of intelligence, locked away from AI + +Brevo's sales organization prospects across dozens of industries. E-commerce, retail, SaaS, financial services. For every prospect call, a rep needs context: which existing Brevo customers are in the same industry? What use cases worked? Has this prospect talked to Brevo before? How far did they get? + +All of that intelligence sits in the CRM. Contacts, Deals, Companies. Years of account history. + +Alexandre Le Goupil runs Revenue Systems and AI for Brevo's sales team. His group had adopted **[Dust](https://dust.tt), an AI agent platform**, and the agents were already good at research, reasoning, and writing. But the CRM was completely out of reach, and that data was what would make the agents' output truly actionable. + +
+ Salespeople would come to me and ask for a list of our best e-commerce customers before a call. Or + they would need to know if we had history with a prospect's company. That context was all in the + CRM, but our AI agents were completely blind to it. I needed to connect those two worlds. ++ +Other teams at Brevo ran their analytics on BigQuery, Snowflake, and Databricks. None of those were built for what Alexandre needed: a database that AI agents could read from and write to in real time, through a standard protocol, without a custom integration for every workflow. + +## Why Supabase + +Alexandre found his answer in the [Supabase MCP server](/docs/guides/getting-started/mcp). MCP (Model Context Protocol) gives AI agents a structured way to interact with databases. Supabase ships one out of the box. That meant a direct connection between Dust's AI agents and a Postgres database, with read and write permissions controlled at the tool level. + +
+ Supabase shipping a remote MCP server means any Dust agent can connect to it instantly, with read + and write access, no custom integration needed. That is exactly the model we want to see from data + platforms: natively agent-ready, so teams can focus on building workflows instead of plumbing. ++ +According to Dust's engineering team, Supabase was the first data platform where they enabled both read and write capabilities through natural language. Snowflake has since followed with its own MCP, and Databricks is next. But Supabase got there first, and for teams like Alexandre's that need to iterate fast with AI, that head start mattered. + +
+ BigQuery and Snowflake are great for analytics. But I needed something an AI agent could query + live, in the middle of a conversation. Supabase gave me Postgres with an MCP server ready to go. + That was what decided it. ++ +Brevo actually started with a custom MCP connection before Dust launched its official Supabase integration in June 2025. When the official version shipped, they migrated. The fact that they went from a scrappy custom setup to the supported integration says something about commitment: this was not an experiment. + +The team is Revenue Operations, not database engineering. Speed mattered. Alexandre connected Supabase to Dust in days. He defined his tables, wrote descriptions of every field so the LLM would know what each one meant, and started testing. + +
+ Connecting Supabase was the easy part. The real work was writing good documentation for the AI, + telling it what each field means, which tables to query for which questions. Once we got that + right, everything clicked. ++ +The Supabase MCP was well-documented and stable, and the integration was live on the platform level within minutes. Alexandre's investment in that documentation is what separates a good Dust agent from a great one. + +## Three workflows in production + +### Finding reference customers in seconds + +Brevo's sales reps now ask a Dust agent: "Find me the top three e-commerce customers we can reference for this prospect." The agent queries the CRM mirror in Supabase, pulls matching customers by industry, deal size, and product usage, and suggests a specific approach angle. + +Before, that request went to RevOps. Someone would pull a report, filter it, and send it back. Fifteen minutes per request, minimum. + +
+ That workflow alone changed how reps prepare for calls. They get reference customers, account + context, and a suggested angle in seconds. It freed up hours of my team's time every week. ++ +### Personalized emails for every prospect + +This is the most ambitious workflow. BDRs select contacts to prospect for the week. That selection kicks off a Dust conversation, which pulls the prospect's full history from Supabase: prior contact, pipeline stage, products discussed, engagement with Brevo's content. + +The agent also grabs context from the web. LinkedIn profiles, firmographic data, company news. Then it routes to a specialized sub-agent based on the prospect type. Someone who downloaded a white paper goes to a "Gated Asset" agent. A cold e-commerce lead goes to an industry-specific agent. Each sub-agent writes three emails tailored to that person's role, seniority, and history. + +The emails come back as structured JSON and HTML. Dust writes them directly to Supabase. Brevo's CRM then pulls those emails into multi-channel sales sequences: email, phone, LinkedIn. + +
+ We used to send the same generic email to every e-commerce prospect. Now every email reflects who + this person is and what we know about them. Supabase holds the context going in and stores the + output coming out. It is the connective tissue between our CRM and our AI. ++ +The result is an 80% reduction in time spent on email personalization, from thirty minutes of research and writing per prospect down to minutes. + +The team also built anti-hallucination guardrails into their prompts. A sales email with a fabricated customer name or made-up statistic destroys trust. By grounding every agent in structured data from Supabase, they reduced that risk. The prompts enforce strict rules: only reference data that exists in the database. + +### Personalized landing pages on demand + +Brevo's marketing team uses the same architecture for lead generation. A visitor enters their email and company name on a [landing page](https://www.brevo.com/tools/relationship-plan?utm_medium=web-external&utm_source=CTA_dust_blog&utm_campaign=en_supabase_blog_202602). The data goes into Supabase and triggers a Dust agent that generates a complete marketing plan for that company: channel recommendations, campaign ideas, timelines. The output lands back in Supabase and renders as a unique page for that visitor. + +
+ Someone fills out a form and gets a [custom marketing + plan](https://www.brevo.com/tools/relationship-plan?utm_medium=web-external&utm_source=CTA_dust_blog&utm_campaign=en_supabase_blog_202602) + in seconds. Data goes into Supabase, the AI builds the plan, the result comes back through + Supabase as a page. We kept the whole thing as simple as we could. ++ +## Set it up, never touch it again + +The part of this story that stands out most is what happened after launch. Nothing. + +Alexandre's team connected Supabase, documented their schema, tested the workflows, and moved on. They have not had to troubleshoot, reconfigure, or debug the connection since. + +
+ We set it up, it worked, and we have not gone back. It just runs. That is exactly what you want + from infrastructure when you are a small ops team trying to ship fast. ++ +Dust's data backs this up. Since the official integration launched in June 2025, Brevo has executed over 2,500 actions through the Supabase MCP. That includes parallel batch runs where dozens of AI conversations query and write to the database at the same time. + +
+ Brevo runs parallel batch conversations making Supabase queries, and it works smoothly. The MCP + has been rock-solid since deployment with no incidents. ++ +Because Supabase is Postgres under the hood, extending it is simple. A new use case means a new table and an updated agent prompt. No re-architecture, no new integrations. + +## An ops team that ships like engineers + +Brevo's Revenue Operations team is not an engineering team. They do not write backend services or manage infrastructure. But with Supabase and [Dust](https://dust.tt), they built three production AI workflows that run daily across Sales and Marketing, without filing a single ticket with engineering. + +This is the pattern Supabase sees across enterprise innovation teams: when you give non-engineering builders a production-grade backend with AI-native tooling, they stop waiting and start shipping. + +
+ What makes Supabase particularly exciting is its agility. It is lighter and faster to iterate with + than traditional data platforms. For AI use cases, this speed matters: storing agent outputs, + syncing workflows, generating content that feeds directly into production systems. It is perfectly + suited for teams that need to experiment rapidly with AI. ++ +## Supabase is the platform for building agentic systems + +Dust's engineering team points to three things that make Supabase work for agents. The remote MCP server is production-grade and easy to onboard. Read and write access closes the agentic loop, so agents produce durable outputs, not just answers. And non-technical teams own the entire data layer themselves, no engineering bottleneck. For agentic use cases where iteration speed is the advantage, that is a structural edge. + +
+ What teams like Brevo's gain is not just speed, it is focus. When agents handle the data pulling, + the personalization, the logging, the people can spend their time where it actually matters: + strategy, relationships, decisions. That is the version of AI we are building toward. ++ +## What comes next + +Alexandre estimates that 30% or more of the internal support requests his team fields via Slack could be answered automatically. The data already exists across Notion, Slack history, and their Supabase-backed CRM mirror. The team is building a Slack assistant that handles first-level responses to internal sales support tickets. + +They are also scaling the email generation workflow to handle thousands of prospects in a single batch run. + +
+ We started with one use case. Now we have three in production and more on the way. Every time we + have a new idea, the first question is: what table do we need in Supabase? That is how fast we can + move now. +diff --git a/apps/www/_customers/hyper.mdx b/apps/www/_customers/hyper.mdx new file mode 100644 index 00000000000..30907d1356c --- /dev/null +++ b/apps/www/_customers/hyper.mdx @@ -0,0 +1,142 @@ +--- +name: Hyper +title: Hyper builds AI marketing agents on Supabase +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: +description: Hyper is an AI-native marketing platform with agents that operate across the entire marketing workflow. Supabase gives their three-person team the database platform to do it at enterprise scale. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: How Hyper's three-person team built an AI marketing platform with agents that handle ads, SEO, and analytics — powered by Supabase at enterprise scale. +author: prashant +author_title: Prashant Sridharan +author_url: https://github.com/CoolAssPuppy +author_image_url: https://avatars.githubusercontent.com/u/914007?v=4 +logo: /images/customers/logos/hyper.svg +logo_inverse: /images/customers/logos/light/hyper.svg +tags: + - supabase +date: '2025-12-19' +company_url: https://www.hyperfx.ai +misc: + - { label: 'Company size', text: 'Pre-seed startup, 3 founders' } + - { label: 'Use case', text: 'Multi-tenant AI agent platform for marketing agencies and SMBs' } +about: AI-native marketing platform with agents that handle paid ads, SEO, analytics, and operations end to end. Connects to 80+ tools with built-in SEO and AI search data. Built for agencies managing hundreds of client accounts, SMBs running ads in-house, and founders launching their first campaign. +# "healthcare" | "fintech" | "ecommerce" | "education" | "gaming" | "media" | "real-estate" | "saas" | "social" | "analytics" | "ai" | "developer-tools" +industry: ['ai', 'saas'] +# "startup" | "enterprise" | "indie_dev" +company_size: 'startup' +# "Asia" | "Europe" | "North America" | "South America" | "Africa" | "Oceania" +region: 'North America' +# "database" | "auth" | "storage" | "realtime" | "functions" | "vector" +supabase_products: ['database', 'auth'] +--- + +
+ AI lowers the cost to start companies. Every company needs marketing. They'll all choose the best + tool, the one that does it for them. We believe the next decade will be won by entrepreneurs and + lean teams, and they'll use Hyper to do it. ++ +[Hyper](https://www.hyperfx.ai) is an AI-native marketing platform with agents that handle paid ads, SEO, analytics, and operations, end to end. Campaigns across Meta, Google, TikTok, and LinkedIn. SEO and content strategy. Cross-platform reporting. The platform connects to 80+ tools and includes built-in capabilities most companies pay separately for: SEO and AI search data on par with SEMrush and Profound, managed databases per workspace, and a native file system where both users and agents can organize assets. + +Hyper is built for anyone who needs marketing — agencies managing hundreds of client accounts, SMBs running ads in-house, founders launching their first campaign. Founded by Elliot Fleck, Themba Mahlangu, and Jasper Shine, the company is building for a market they see as massive and inevitable. + +## The challenge + +Marketing is fragmented. Every platform — Meta, Google, Shopify, ESPs — has its own dashboard, its own metrics, its own API. Pulling that data into a single view is one of the biggest pain points in the industry — not just because it's tedious and fragile, but because without that context, you can't act correctly. Every decision depends on seeing the full picture. And it's getting harder. How people find products is changing — search to social feeds to AI assistants — tracking is noisier, and execution breaks down as complexity grows. + +Without a unified action system and data layer, problems go unnoticed. A broken campaign. A budget overspend. A drop in conversions that nobody catches for weeks. + +
+ Whether you're a first-time founder or an agency managing hundreds of accounts, your data is + spread across every platform — and no one knows if something is broken until weeks later. Agents + on Hyper consolidate all of it and catch problems immediately, like an expert watching everything + 24/7. AI is changing how work gets done — people want to say 'launch this' and 'run analysis and + email me the report,' not do it themselves. ++ +## Why they chose Supabase + +Hyper needed a database platform that could handle multi-tenant data at scale, give both technical and non-technical team members direct visibility, and work well with AI agents. Supabase checked every box. + +The Supabase dashboard sits directly on top of the database. Elliot checks signup numbers by opening a table and sorting by date. Themba verifies a data sync by looking at the same table. No internal admin dashboards to build. No secondary tooling to maintain. + +
+ What sets Supabase apart is its UI. I can get in there and look at the database. Elliot, as the + non-technical co-founder, can also get in there and really dig deep without having to consult me + about anything. ++ +Beyond the database, Supabase gives Hyper a full platform to grow into. Auth, Realtime, and Storage are all available without adding new vendors or stitching together separate services. + +
+ It's clear what Supabase is building for. They're building databases and features in the age of + super intelligence. Fantastic community, amazing developer documentation. If you're a startup + looking to build the next unicorn or just a lifestyle business, it's the easiest way to get + started and scale. ++ +## The solution + +Hyper built a real-time data pipeline that automatically syncs cross-platform marketing data into Supabase — Meta, Google Ads, GA4, Search Console and more. Each customer gets their own managed Supabase database with only their data, and all they have to do is connect their account. No setup, no Supermetrics, no ETL, no warehouse configuration. It's the entire marketing data stack, simplified into one product that does all the heavy lifting. + +From there, agents run SQL to analyze marketing data, spin up customizable dashboards, and surface insights. No intermediate layer, no stale cache — agents query the real data directly. + +
+ Rather than going out in real time to each platform to get insights, agents write SQL to query the + data themselves. Agents are great at writing SQL. It's way more efficient. A literal 100x cost + reduction compared with if the agent were to go out and try to get that data. ++ +Asking an agent to call APIs across five ad platforms in real time is slow, expensive, and often impossible at scale. But an agent can write a SQL query against a synced database and get the answer in seconds. The data stays close, structured, and queryable. Agents reason over it with SQL rather than wrestling with rate-limited APIs. + +When agents query Supabase, they hit the same tables that the founders and customers inspect in the dashboard. No ETL pipeline introducing drift. No caching layer serving stale results. The source of truth is the same for humans and agents. As the founders put it, "AI agents know Supabase like the back of their hand." + +
+ If you're getting the data from one place and visualizing it elsewhere, there's always going to be + a question mark. With Supabase, any user, whether they're technical or non-technical, can easily + jump in and see what's going on. ++ +The result is a platform where agents operate like experts. Whether the user is a seasoned marketer or a first-time business owner, the agents generate reports, optimize campaigns, and flag issues automatically. + +## The results + +With Supabase as the system of record, Hyper gives agents a 360° marketing OS — unifying data, integrations, and intelligence into one product they can operate instantly, from analytics to real execution. Agents catch what's changing, diagnose opportunities and issues, and take omnichannel action immediately — across paid ads, SEO, reporting, and beyond. + +- 100x cost reduction in data retrieval by having agents query SQL instead of calling platform APIs in real time +- 80+ tool integrations connected through a single data pipeline +- Per-customer managed databases with isolated data for each client +- Continuous data sync across all connected ad platforms and data sources (real-time + scheduled) + +## What's next + +Hyper's next phase is built on Supabase for Platforms. The architecture creates two layers. + +The first layer handles the heavy internal workload: the pipeline that syncs ad platform data for agencies and powers the AI agents that analyze it. This runs on larger Supabase instances. + +The second layer changes the product. Each agency's end clients get their own isolated database through Supabase for Platforms. Today, agencies use Hyper to expose reporting dashboards for their clients. With Supabase for Platforms, they can expose full applications. + +
+ Supabase for Platforms opens up a path for our customers beyond just dashboards. They can expose + richer apps. We can take advantage of all the features that Supabase for Platforms offers, like + Storage, like rich authentication, for their end users. ++ +Supabase for Platforms handles the backend infrastructure: per-customer databases, isolated storage, and authentication logic that would have taken at least two weeks to build, even with pre-built libraries. It also provides frontend components that plug directly into Hyper's app. That frees the team to focus on what sits on top: the agents, the dashboards, and the workflows their customers actually see. + +The vision goes further. If marketing agencies can spin up websites for their end clients through Hyper, all powered by Supabase for Platforms, that opens an entirely new competitive front. + +
+ With Supabase for Platforms, anyone can come into Hyper and launch a marketing agency from + scratch. Build a website, set up authentication and contact forms, create a funnel. In under 15 + minutes you have a live agency with AI agents that intake clients and run their marketing end to + end. ++ +That is what happens when you give a product team a platform that handles databases, auth, storage, and real-time out of the box. They stop building infrastructure and start building the thing they set out to build. + +
+ I will get on a podcast and talk about how much I love Supabase and the whole experience. With + Supabase we can move fast and build things that delight our customers without having to worry + about infrastructure. +diff --git a/apps/www/_go/events/accenture-reinvention-2026/contest.tsx b/apps/www/_go/events/accenture-reinvention-2026/contest.tsx new file mode 100644 index 00000000000..3209290b4de --- /dev/null +++ b/apps/www/_go/events/accenture-reinvention-2026/contest.tsx @@ -0,0 +1,61 @@ +import type { GoPageInput } from 'marketing' +import Link from 'next/link' +import { Button } from 'ui' + +const page: GoPageInput = { + template: 'lead-gen', + slug: 'accenture-reinvention-2026/contest', + metadata: { + title: + 'Win an iPhone 17 Pro Max | Supabase at Accenture AI & Data Conference (ReinventionX) 2026', + description: + 'Create a Supabase account and load data for a chance to win an iPhone 17 Pro Max. Accenture AI & Data Conference (ReinventionX) 2026.', + }, + hero: { + title: 'Win an iPhone 17 Pro Max', + subtitle: 'Supabase at Accenture AI & Data Conference 2026', + description: + 'Your team is already building with AI tools. Supabase is the production backend that turns those prototypes into secure, scalable applications. Try it out -- create an account, load some data, and you could win an iPhone 17 Pro Max.', + image: { + src: '/images/landing-pages/stripe-sessions/iphone17-pro-max.png', + alt: 'Orange iPhone 17 Pro Max', + width: 400, + height: 500, + }, + ctas: [ + { + label: 'Get started', + href: '#how-to-enter', + variant: 'primary', + }, + ], + }, + sections: [ + { + type: 'single-column', + id: 'how-to-enter', + title: 'How to enter', + children: ( +
+ No purchase necessary. Void where prohibited.{' '} + + Official rules + + . +
++ No purchase necessary. Void where prohibited.{' '} + + Official rules + + . +
++ No purchase necessary. Void where prohibited.{' '} + + Official rules + + . +
++ No purchase necessary. Void where prohibited.{' '} + + Official rules + + . +
+Spruce Restaurant
+3640 Sacramento St, San Francisco, CA
+Wednesday, April 29, 2026
+6:30 PM -- Cocktails and introductions
+7:00 PM -- Dinner and discussion
+VP, Supabase
++ Head of Databases, Supabase +
++ Head of Multigres, Supabase +
++ No purchase necessary. Void where prohibited.{' '} + + Official rules + + . +
+- This user hasn't created any threads or replies yet, or doesn't exist. + This user hasn’t created any threads or replies yet, or doesn’t exist.
- -
- by{' '}
-
+ {/* Platform, Date, Author, Title */}
+