From 778a0945264d23e472569e53d3633814752e991b Mon Sep 17 00:00:00 2001 From: ArthurGamby Date: Thu, 2 Apr 2026 16:27:50 +0200 Subject: [PATCH 1/6] fix(site): add currency conversion to pricing page Previously the currency selector only swapped the symbol without converting the amount. Now applies approximate exchange rates so prices display realistic converted values with clean rounding. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/pricing/pricing-calculator.tsx | 450 +++++++++--------- apps/site/src/app/pricing/pricing-data.ts | 93 ++-- 2 files changed, 276 insertions(+), 267 deletions(-) diff --git a/apps/site/src/app/pricing/pricing-calculator.tsx b/apps/site/src/app/pricing/pricing-calculator.tsx index be232474f3..16093eb1ab 100644 --- a/apps/site/src/app/pricing/pricing-calculator.tsx +++ b/apps/site/src/app/pricing/pricing-calculator.tsx @@ -16,6 +16,8 @@ import { type Symbol, symbols, usagePricing, + convertFromUsd, + currencyConfig, } from "./pricing-data"; function cn(...classes: Array) { @@ -66,25 +68,29 @@ const PRESETS: Record< }, }; -const CALCULATOR_PLAN_ORDER = Object.keys( - usagePricing, -) as BillablePricingPlanKey[]; +const CALCULATOR_PLAN_ORDER = Object.keys(usagePricing) as BillablePricingPlanKey[]; function formatNumber(value: number) { return new Intl.NumberFormat("en-US").format(Math.round(value)); } -function formatCurrency(value: number, currency: Symbol, digits = 2) { - return `${symbols[currency]}${value.toLocaleString("en-US", { - minimumFractionDigits: digits, - maximumFractionDigits: digits, +function formatCurrency(valueUsd: number, currency: Symbol, digits = 2) { + const converted = convertFromUsd(valueUsd, currency); + const rounded = converted >= 1 ? Math.round(converted) : converted; + const effectiveDigits = converted >= 1 ? 0 : digits; + return `${symbols[currency]}${rounded.toLocaleString("en-US", { + minimumFractionDigits: effectiveDigits, + maximumFractionDigits: effectiveDigits, })}`; } -function formatCompactCurrency(value: number, currency: Symbol) { - return `${symbols[currency]}${value.toLocaleString("en-US", { - minimumFractionDigits: Number.isInteger(value) ? 0 : 1, - maximumFractionDigits: 2, +function formatCompactCurrency(valueUsd: number, currency: Symbol) { + const converted = convertFromUsd(valueUsd, currency); + const config = currencyConfig[currency]; + const maxDigits = Number.isInteger(converted) && converted > 1 ? 0 : config.microDecimals; + return `${symbols[currency]}${converted.toLocaleString("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: maxDigits, })}`; } @@ -113,7 +119,7 @@ function calculateMonthlyPlanCost( return ( details.baseMonthlyPrice + - extraOperations / 1_000 * details.operationPricePerThousand + + (extraOperations / 1_000) * details.operationPricePerThousand + extraStorageGb * details.storagePricePerGb ); } @@ -140,16 +146,11 @@ function calculatePlanBreakdown( billingCycle: BillingCycle, ): CostBreakdown { const details = usagePricing[plan]; - const billableOperations = Math.max( - 0, - databaseOperations - details.includedOperations, - ); + const billableOperations = Math.max(0, databaseOperations - details.includedOperations); const billableStorageGb = Math.max(0, storageGb - details.includedStorageGb); - const operationsCost = - billableOperations / 1_000 * details.operationPricePerThousand; + const operationsCost = (billableOperations / 1_000) * details.operationPricePerThousand; const storageCost = billableStorageGb * details.storagePricePerGb; - const yearlyMultiplier = - billingCycle === "yearly" ? 1 - details.yearlyDiscount : 1; + const yearlyMultiplier = billingCycle === "yearly" ? 1 - details.yearlyDiscount : 1; return { basePlanFee: details.baseMonthlyPrice * yearlyMultiplier, @@ -187,28 +188,16 @@ function getRecommendedPlan( }); } -function getMatchingPreset( - databaseOperations: number, - storageGb: number, -): PresetKey | null { - const match = ( - Object.entries(PRESETS) as Array<[PresetKey, (typeof PRESETS)[PresetKey]]> - ).find( +function getMatchingPreset(databaseOperations: number, storageGb: number): PresetKey | null { + const match = (Object.entries(PRESETS) as Array<[PresetKey, (typeof PRESETS)[PresetKey]]>).find( ([, preset]) => - preset.databaseOperations === databaseOperations && - preset.storageGb === storageGb, + preset.databaseOperations === databaseOperations && preset.storageGb === storageGb, ); return match?.[0] ?? null; } -function InputShell({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { +function InputShell({ children, className }: { children: React.ReactNode; className?: string }) { return (
)}
-

+

- First {formatNumber(planDetails.includedStorageGb)}GB of - storage are included. Remaining{" "} - {formatNumber(breakdown.billableStorageGb)}GB are billed at{" "} + First {formatNumber(planDetails.includedStorageGb)}GB of storage are included. + Remaining {formatNumber(breakdown.billableStorageGb)}GB are billed at{" "} {formatCompactCurrency(planDetails.storagePricePerGb, currency)} /GB. @@ -382,12 +368,9 @@ function SummaryCard({ } export function PricingCalculator({ currency }: { currency: Symbol }) { - const [lastAppliedPreset, setLastAppliedPreset] = - React.useState("scaleup"); - const [billingCycle, setBillingCycle] = - React.useState("monthly"); - const [expandedPlan, setExpandedPlan] = - React.useState(null); + const [lastAppliedPreset, setLastAppliedPreset] = React.useState("scaleup"); + const [billingCycle, setBillingCycle] = React.useState("monthly"); + const [expandedPlan, setExpandedPlan] = React.useState(null); const [databaseOperations, setDatabaseOperations] = React.useState( PRESETS.scaleup.databaseOperations, ); @@ -418,116 +401,116 @@ export function PricingCalculator({ currency }: { currency: Symbol }) { return (

-
-
-

- Pricing Calculator -

- -
-
- Quick Start Presets -
-
- {(Object.entries(PRESETS) as Array<[PresetKey, (typeof PRESETS)[PresetKey]]>).map( - ([key, item]) => { - const active = key === matchingPreset; - - return ( - - ); - }, - )} +
+
+

+ Pricing Calculator +

+ +
+
+ Quick Start Presets +
+
+ {(Object.entries(PRESETS) as Array<[PresetKey, (typeof PRESETS)[PresetKey]]>).map( + ([key, item]) => { + const active = key === matchingPreset; + + return ( + + ); + }, + )} +
-
-
-
-
- -

- Estimate your monthly usage -

- -
+
+
+
+ +

+ Estimate your monthly usage +

+ +
-
-
-
- - Database Operations - +
+
+
+ + Database Operations + +
+ {formatNumber(databaseOperations)} + setDatabaseOperations(value[0] ?? databaseOperations)} + />
- {formatNumber(databaseOperations)} - setDatabaseOperations(value[0] ?? databaseOperations)} - /> -
-
-
- Estimated SQL Queries - - {SQL_QUERY_MULTIPLIER}x - - +
+
+ Estimated SQL Queries + + {SQL_QUERY_MULTIPLIER}x + + +
+ {formatNumber(estimatedSqlQueries)}
- {formatNumber(estimatedSqlQueries)} -
-
-
- - Storage +
+
+ + Storage +
+ + {formatNumber(storageGb)} + + GB + + + setStorageGb(value[0] ?? storageGb)} + />
- - {formatNumber(storageGb)} - - GB - - - setStorageGb(value[0] ?? storageGb)} - /> -
-
- {/*
+
+ {/*
Compute Size
Included and auto-scaled by Prisma Postgres @@ -537,98 +520,93 @@ export function PricingCalculator({ currency }: { currency: Symbol }) {

*/} -
-
Data Transfer
-
- Unlimited included for free +
+
Data Transfer
+
+ Unlimited included for free +
+

+ Ingress, egress, sidewaysgress, it's all covered. Just Ship It. +

-

- Ingress, egress, sidewaysgress, it's all covered. Just Ship It. -

-
-
-
-
- -

- Estimated total cost -

-
+
+
+
+ +

+ Estimated total cost +

+
+ +
+ {(["monthly", "yearly"] as BillingCycle[]).map((cycle) => { + const active = cycle === billingCycle; -
- {(["monthly", "yearly"] as BillingCycle[]).map((cycle) => { - const active = cycle === billingCycle; - - return ( - - ); - })} + return ( + + ); + })} +
-
-
- {isEnterpriseRecommendation && ( - -

- Usage at this scale is best served on an enterprise plan. Reach - out to{" "} - - support@prisma.io - {" "} - for pricing. -

-
- )} - {CALCULATOR_PLAN_ORDER.map((plan) => ( - - setExpandedPlan((current) => (current === plan ? null : plan)) - } - yearly={billingCycle === "yearly"} - price={calculateDisplayedPlanCost( - plan, - databaseOperations, - storageGb, - billingCycle, - )} - /> - ))} +
+ {isEnterpriseRecommendation && ( + +

+ Usage at this scale is best served on an enterprise plan. Reach out to{" "} + + support@prisma.io + {" "} + for pricing. +

+
+ )} + {CALCULATOR_PLAN_ORDER.map((plan) => ( + setExpandedPlan((current) => (current === plan ? null : plan))} + yearly={billingCycle === "yearly"} + price={calculateDisplayedPlanCost( + plan, + databaseOperations, + storageGb, + billingCycle, + )} + /> + ))} +
-
); } diff --git a/apps/site/src/app/pricing/pricing-data.ts b/apps/site/src/app/pricing/pricing-data.ts index fa87098395..0f520fb830 100644 --- a/apps/site/src/app/pricing/pricing-data.ts +++ b/apps/site/src/app/pricing/pricing-data.ts @@ -11,17 +11,37 @@ export const symbols = { USD: "$", } as const; -export type Symbol = - | "EUR" - | "AUD" - | "INR" - | "GBP" - | "CAD" - | "BRL" - | "JPY" - | "CNY" - | "KRW" - | "USD"; +export type Symbol = "EUR" | "AUD" | "INR" | "GBP" | "CAD" | "BRL" | "JPY" | "CNY" | "KRW" | "USD"; + +export const exchangeRates: Record = { + USD: 1, + EUR: 0.92, + GBP: 0.79, + AUD: 1.55, + CAD: 1.37, + INR: 83.5, + BRL: 5.0, + JPY: 151, + CNY: 7.25, + KRW: 1350, +}; + +export const currencyConfig: Record = { + USD: { decimals: 2, microDecimals: 4 }, + EUR: { decimals: 2, microDecimals: 4 }, + GBP: { decimals: 2, microDecimals: 4 }, + AUD: { decimals: 2, microDecimals: 4 }, + CAD: { decimals: 2, microDecimals: 4 }, + INR: { decimals: 0, microDecimals: 2 }, + BRL: { decimals: 2, microDecimals: 4 }, + JPY: { decimals: 0, microDecimals: 2 }, + CNY: { decimals: 2, microDecimals: 4 }, + KRW: { decimals: 0, microDecimals: 2 }, +}; + +export function convertFromUsd(amountUsd: number, currency: Symbol): number { + return amountUsd * exchangeRates[currency]; +} export type CurrencyMap = Record; @@ -30,7 +50,7 @@ export type PlanPoint = | { text: string; price: CurrencyMap; - } + }; export type PricingPlan = { title: string; @@ -48,16 +68,23 @@ export type UsagePricing = { yearlyDiscount: number; }; -/** Formats a single numeric amount with every supported currency symbol. */ -export function formatAmountForAllCurrencies( - amount: number, - digits: number, -): CurrencyMap { +export function formatAmountForAllCurrencies(amountUsd: number, digits: number): CurrencyMap { return Object.fromEntries( - Object.entries(symbols).map(([code, symbol]) => [ - code, - `${symbol}${amount.toFixed(digits)}`, - ]), + Object.entries(symbols).map(([code, symbol]) => { + const typedCode = code as Symbol; + const converted = convertFromUsd(amountUsd, typedCode); + const config = currencyConfig[typedCode]; + const isMicroPrice = digits > config.decimals; + const effectiveDigits = isMicroPrice ? config.microDecimals : digits; + const displayValue = isMicroPrice ? converted : Math.round(converted); + return [ + code, + `${symbol}${displayValue.toLocaleString("en-US", { + minimumFractionDigits: effectiveDigits, + maximumFractionDigits: effectiveDigits, + })}`, + ]; + }), ) as CurrencyMap; } @@ -189,15 +216,19 @@ export const comparisonSections = [ { title: "Global Cache", rows: [ - ["Cache tag invalidations", "-", "-", "$0.002 per 1,000, max 10,000 per day", "$0.001 per 1,000, max 100,000 per day"], + [ + "Cache tag invalidations", + "-", + "-", + "$0.002 per 1,000, max 10,000 per day", + "$0.001 per 1,000, max 100,000 per day", + ], ["Cache purge requests", "5 per hour", "5 per hour", "10 per hour", "20 per hour"], ], }, { title: "Database optimizations", - rows: [ - ["Query insights", "✓", "✓", "✓", "✓"], - ], + rows: [["Query insights", "✓", "✓", "✓", "✓"]], }, { title: "Data management", @@ -216,12 +247,12 @@ export const faqs: Array<{ question: string; answer: string }> = [ { question: "What is an operation?", answer: - "

Each action you perform, whether it’s a create, read, update, or delete query against your Prisma Postgres database counts as a single operation. Even if Prisma issues multiple database queries behind the scenes to fulfill your request, it’s still billed as one operation.

By treating simple lookups and complex queries the same, you can directly correlate your database usage and costs with your product usage and user behavior. There’s no need to track write-heavy workloads or worry about bandwidth per operation: each of them is counted and billed the same, making your usage and budgeting simple and straightforward. You can learn more about our operations-based pricing model in our blog post.

", + '

Each action you perform, whether it’s a create, read, update, or delete query against your Prisma Postgres database counts as a single operation. Even if Prisma issues multiple database queries behind the scenes to fulfill your request, it’s still billed as one operation.

By treating simple lookups and complex queries the same, you can directly correlate your database usage and costs with your product usage and user behavior. There’s no need to track write-heavy workloads or worry about bandwidth per operation: each of them is counted and billed the same, making your usage and budgeting simple and straightforward. You can learn more about our operations-based pricing model in our blog post.

', }, { question: "How many operations do I need for my project?", answer: - "

While the answer to this question will vary from project to project, there are a couple of ways to get an idea of what you will need:

  • If you already have a database with another provider, you can often look in their dashboard to see your current usage. The number of queries will be a good hint to the approximate number of operations you’ll use.
  • If you already use the Prisma ORM, you can enable the metrics feature to begin tracking your usage, which is an easy and accurate way to see your current usage.
  • If you’re starting a new project, we encourage you to just get started and see how many queries you typically use. We offer a free plan with 100,000 operations per month, meaning you can confidently get started without paying anything. From our experience, 100,000 operations per month is more than enough to get started with a project and serve your first users.

You can find an example calculation for a medium-sized workload in our blog post about our operations-based pricing model.

", + '

While the answer to this question will vary from project to project, there are a couple of ways to get an idea of what you will need:

  • If you already have a database with another provider, you can often look in their dashboard to see your current usage. The number of queries will be a good hint to the approximate number of operations you’ll use.
  • If you already use the Prisma ORM, you can enable the metrics feature to begin tracking your usage, which is an easy and accurate way to see your current usage.
  • If you’re starting a new project, we encourage you to just get started and see how many queries you typically use. We offer a free plan with 100,000 operations per month, meaning you can confidently get started without paying anything. From our experience, 100,000 operations per month is more than enough to get started with a project and serve your first users.

You can find an example calculation for a medium-sized workload in our blog post about our operations-based pricing model.

', }, { question: "Can I use Prisma Postgres for free?", @@ -241,12 +272,12 @@ export const faqs: Array<{ question: string; answer: string }> = [ { question: "What’s the difference between usage pricing and traditional database pricing?", answer: - "

Traditional pricing is where you choose a fixed database size and price, and the amount you pay is generally predictable. But that comes at the expense of flexibility, meaning it’s much harder to scale up and down with your application’s demands. This is usually fine for a small test database, but for production workloads, it can be burdensome: If you have low-traffic periods, and high-traffic periods (most production apps do) then you either under-provision and risk having downtime in busy periods, or you over-provision and pay a lot more for your database.

With usage pricing, you only pay for what you need, when you need it. If your app has a quiet period, you’ll pay less. If things get busy, we can simply scale up to handle it for you. Prisma Postgres comes with budget controls, so you can always stay in control of your spending, while taking advantage of the flexibility. You can learn more on why operations-based pricing is better in our blog post.

", + '

Traditional pricing is where you choose a fixed database size and price, and the amount you pay is generally predictable. But that comes at the expense of flexibility, meaning it’s much harder to scale up and down with your application’s demands. This is usually fine for a small test database, but for production workloads, it can be burdensome: If you have low-traffic periods, and high-traffic periods (most production apps do) then you either under-provision and risk having downtime in busy periods, or you over-provision and pay a lot more for your database.

With usage pricing, you only pay for what you need, when you need it. If your app has a quiet period, you’ll pay less. If things get busy, we can simply scale up to handle it for you. Prisma Postgres comes with budget controls, so you can always stay in control of your spending, while taking advantage of the flexibility. You can learn more on why operations-based pricing is better in our blog post.

', }, { question: "How is Prisma’s pricing different to others?", answer: - "

Prisma’s pricing is designed to provide maximum flexibility to developers, while aiming to be as intuitive as possible.

We charge primarily by operation, which is counted each time you invoke the Prisma ORM client to create, read, update or delete a record. Additionally we also charge for storage. All with a very generous free threshold each month.

We don’t charge by data transfer (bandwidth) or by compute/memory hours, simply because we felt that these metrics are more difficult to grasp as a developer.

We created a pricing model to more closely match how you use your database as a developer, not how the infrastructure works. You can learn more about our approach to an operations-based database pricing model in this blog post.

", + '

Prisma’s pricing is designed to provide maximum flexibility to developers, while aiming to be as intuitive as possible.

We charge primarily by operation, which is counted each time you invoke the Prisma ORM client to create, read, update or delete a record. Additionally we also charge for storage. All with a very generous free threshold each month.

We don’t charge by data transfer (bandwidth) or by compute/memory hours, simply because we felt that these metrics are more difficult to grasp as a developer.

We created a pricing model to more closely match how you use your database as a developer, not how the infrastructure works. You can learn more about our approach to an operations-based database pricing model in this blog post.

', }, { question: "How can I compare Prisma pricing to other providers?", @@ -261,11 +292,11 @@ export const faqs: Array<{ question: string; answer: string }> = [ { question: "I'm an early stage startup, do you offer any discounts?", answer: - "

Building a startup is hard. Prisma helps you stay laser-focused on what matters the most, which is building features and winning users.

We offer $10k in credits to eligible startups. Learn more at prisma.io/startups.

", + '

Building a startup is hard. Prisma helps you stay laser-focused on what matters the most, which is building features and winning users.

We offer $10k in credits to eligible startups. Learn more at prisma.io/startups.

', }, { question: "How do I upgrade my plan if I am using Prisma Postgres via Vercel?", answer: - "

If you're using Prisma Postgres via Vercel, your billing is handled directly by Vercel. To upgrade your plan, you'll need to do so in the Vercel Dashboard. The instructions are available in our docs.

", + '

If you\'re using Prisma Postgres via Vercel, your billing is handled directly by Vercel. To upgrade your plan, you\'ll need to do so in the Vercel Dashboard. The instructions are available in our docs.

', }, ]; From 5055b6693d6396849b0e79de1436542f7aaa2249 Mon Sep 17 00:00:00 2001 From: ArthurGamby Date: Thu, 2 Apr 2026 16:32:47 +0200 Subject: [PATCH 2/6] fix(site): align exchange rates with existing website repo Use the same rates as the old website (lib/currency.ts) for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/site/src/app/pricing/pricing-data.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/site/src/app/pricing/pricing-data.ts b/apps/site/src/app/pricing/pricing-data.ts index 0f520fb830..edc664ec5c 100644 --- a/apps/site/src/app/pricing/pricing-data.ts +++ b/apps/site/src/app/pricing/pricing-data.ts @@ -15,15 +15,15 @@ export type Symbol = "EUR" | "AUD" | "INR" | "GBP" | "CAD" | "BRL" | "JPY" | "CN export const exchangeRates: Record = { USD: 1, - EUR: 0.92, - GBP: 0.79, - AUD: 1.55, - CAD: 1.37, - INR: 83.5, - BRL: 5.0, - JPY: 151, - CNY: 7.25, - KRW: 1350, + EUR: 0.93, + GBP: 0.8, + AUD: 1.54, + CAD: 1.36, + INR: 83.2, + BRL: 5.4, + JPY: 157.5, + CNY: 7.2, + KRW: 1370, }; export const currencyConfig: Record = { From bb547b4cdea136b5bace02ec51601025b03f0685 Mon Sep 17 00:00:00 2001 From: ArthurGamby Date: Thu, 2 Apr 2026 17:03:06 +0200 Subject: [PATCH 3/6] fix(site): make comparison table respect currency selection Moved comparison table into a client component so it receives the currency state. Cache tag invalidation prices now convert with the selected currency instead of showing hardcoded USD values. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/site/src/app/pricing/page.tsx | 107 +----------------- .../app/pricing/pricing-comparison-table.tsx | 88 ++++++++++++++ apps/site/src/app/pricing/pricing-data.ts | 19 +++- .../src/app/pricing/pricing-page-content.tsx | 13 +++ 4 files changed, 121 insertions(+), 106 deletions(-) create mode 100644 apps/site/src/app/pricing/pricing-comparison-table.tsx diff --git a/apps/site/src/app/pricing/page.tsx b/apps/site/src/app/pricing/page.tsx index 098036e0aa..3315150b67 100644 --- a/apps/site/src/app/pricing/page.tsx +++ b/apps/site/src/app/pricing/page.tsx @@ -1,26 +1,11 @@ import { JsonLd } from "@/components/json-ld"; import { createFaqStructuredData } from "@/lib/structured-data"; import type { Metadata } from "next"; -import { - Accordion, - Accordions, - Badge, - Button, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@prisma/eclipse"; -import { comparisonSections, faqs } from "./pricing-data"; +import { Accordion, Accordions, Button } from "@prisma/eclipse"; +import { faqs } from "./pricing-data"; import { PricingPageContent } from "./pricing-page-content"; -const pricingFaqStructuredData = createFaqStructuredData( - "/pricing", - faqs, - "Prisma pricing FAQ", -); +const pricingFaqStructuredData = createFaqStructuredData("/pricing", faqs, "Prisma pricing FAQ"); export const metadata: Metadata = { title: "Pricing | Prisma Postgres", @@ -52,87 +37,9 @@ export const metadata: Metadata = { export default function PricingPage() { return (
- + - {/* Compare plans */} -
-
-

- Compare plans -

-

- All of the features below are included with Prisma Postgres. -

-
-
- - - - - {comparisonSections[0]?.title} - - {["Free", "Starter", "Pro", "Business"].map((label) => ( - - - - ))} - - - {comparisonSections.map((section) => ( - - - - {section.title} - - - - {section.rows.map((row) => ( - - - {row[0]} - - {row.slice(1).map((value, valueIndex) => ( - - {value} - - ))} - - ))} - - ))} -
-
-
- {/* FAQ */}
@@ -190,11 +97,7 @@ export default function PricingPage() { Create your first Database - diff --git a/apps/site/src/app/pricing/pricing-comparison-table.tsx b/apps/site/src/app/pricing/pricing-comparison-table.tsx new file mode 100644 index 0000000000..e1d21ea6cf --- /dev/null +++ b/apps/site/src/app/pricing/pricing-comparison-table.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { + Badge, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@prisma/eclipse"; +import { type ComparisonCell, type Symbol, comparisonSections, symbols } from "./pricing-data"; + +function renderCell(cell: ComparisonCell, currency: Symbol): string { + if (typeof cell === "string") return cell; + return cell.text.replace("", `${symbols[currency]}${cell.price[currency]}`); +} + +export function PricingComparisonTable({ currency }: { currency: Symbol }) { + return ( +
+ + + + + {comparisonSections[0]?.title} + + {["Free", "Starter", "Pro", "Business"].map((label) => ( + + + + ))} + + + {comparisonSections.map((section) => ( + + + + {section.title} + + + + {section.rows.map((row) => { + const label = typeof row[0] === "string" ? row[0] : row[0].text; + return ( + + + {label} + + {row.slice(1).map((cell, valueIndex) => ( + + {renderCell(cell, currency)} + + ))} + + ); + })} + + ))} +
+
+ ); +} diff --git a/apps/site/src/app/pricing/pricing-data.ts b/apps/site/src/app/pricing/pricing-data.ts index edc664ec5c..bf0ed81abf 100644 --- a/apps/site/src/app/pricing/pricing-data.ts +++ b/apps/site/src/app/pricing/pricing-data.ts @@ -200,7 +200,12 @@ export const usagePricing: Record = { }, }; -export const comparisonSections = [ +export type ComparisonCell = string | { text: string; price: CurrencyMap }; + +export const comparisonSections: Array<{ + title: string; + rows: ComparisonCell[][]; +}> = [ { title: "Managed Connection Pool", rows: [ @@ -220,8 +225,14 @@ export const comparisonSections = [ "Cache tag invalidations", "-", "-", - "$0.002 per 1,000, max 10,000 per day", - "$0.001 per 1,000, max 100,000 per day", + { + text: " per 1,000, max 10,000 per day", + price: formatAmountForAllCurrencies(0.002, 3), + }, + { + text: " per 1,000, max 100,000 per day", + price: formatAmountForAllCurrencies(0.001, 3), + }, ], ["Cache purge requests", "5 per hour", "5 per hour", "10 per hour", "20 per hour"], ], @@ -241,7 +252,7 @@ export const comparisonSections = [ ["Compliance", "GDPR", "GDPR", "GDPR / HIPAA", "GDPR / HIPAA / SOC2 / ISO:27001"], ], }, -] as const; +]; export const faqs: Array<{ question: string; answer: string }> = [ { diff --git a/apps/site/src/app/pricing/pricing-page-content.tsx b/apps/site/src/app/pricing/pricing-page-content.tsx index 8d9103deee..72e038dc6f 100644 --- a/apps/site/src/app/pricing/pricing-page-content.tsx +++ b/apps/site/src/app/pricing/pricing-page-content.tsx @@ -3,6 +3,7 @@ import * as React from "react"; import type { Symbol } from "./pricing-data"; import { PricingCalculator } from "./pricing-calculator"; +import { PricingComparisonTable } from "./pricing-comparison-table"; import { PricingHeroPlans } from "./pricing-hero-plans"; export function PricingPageContent() { @@ -15,6 +16,18 @@ export function PricingPageContent() {
+ +
+
+

+ Compare plans +

+

+ All of the features below are included with Prisma Postgres. +

+
+ +
); } From a57b42aed75cf439247598ab990296bb67c72580 Mon Sep 17 00:00:00 2001 From: ArthurGamby Date: Thu, 2 Apr 2026 17:04:59 +0200 Subject: [PATCH 4/6] chore: remove Optimize from footer product column Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ui/src/data/footer.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/data/footer.ts b/packages/ui/src/data/footer.ts index 96c8a94043..170786c3e5 100644 --- a/packages/ui/src/data/footer.ts +++ b/packages/ui/src/data/footer.ts @@ -14,13 +14,7 @@ const footerItems = [ url: "/studio", _type: "footerLinkType", }, - { - title: "Optimize", - url: "/optimize", - _type: "footerLinkType", - //tag: "Early Access" - }, - { +{ title: "Accelerate", url: "/accelerate", _type: "footerLinkType", @@ -193,13 +187,8 @@ const shareSocials = [ { label: "LinkedIn", icon: "fa-brands fa-square-linkedin", - url: ({ - current_page, - text_data, - }: { - current_page: string; - text_data: string; - }) => `https://www.linkedin.com/sharing/share-offsite/?url=${current_page}`, + url: ({ current_page, text_data }: { current_page: string; text_data: string }) => + `https://www.linkedin.com/sharing/share-offsite/?url=${current_page}`, }, { label: "X", @@ -220,13 +209,8 @@ const shareSocials = [ { label: "Bluesky", icon: "fa-brands fa-bluesky", - url: ({ - current_page, - text_data, - }: { - current_page: string; - text_data: string; - }) => `https://bsky.app/intent/compose?text=${text_data}${current_page}`, + url: ({ current_page, text_data }: { current_page: string; text_data: string }) => + `https://bsky.app/intent/compose?text=${text_data}${current_page}`, }, { label: "Copy link", icon: "fa-solid fa-link", copy: true }, ]; From b52020919fb2da70bc175494a934febb20cd77d3 Mon Sep 17 00:00:00 2001 From: ArthurGamby Date: Thu, 2 Apr 2026 17:08:02 +0200 Subject: [PATCH 5/6] fix(site): remove duplicate currency symbol in comparison table Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/site/src/app/pricing/pricing-comparison-table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/site/src/app/pricing/pricing-comparison-table.tsx b/apps/site/src/app/pricing/pricing-comparison-table.tsx index e1d21ea6cf..92bae1f65a 100644 --- a/apps/site/src/app/pricing/pricing-comparison-table.tsx +++ b/apps/site/src/app/pricing/pricing-comparison-table.tsx @@ -9,11 +9,11 @@ import { TableHeader, TableRow, } from "@prisma/eclipse"; -import { type ComparisonCell, type Symbol, comparisonSections, symbols } from "./pricing-data"; +import { type ComparisonCell, type Symbol, comparisonSections } from "./pricing-data"; function renderCell(cell: ComparisonCell, currency: Symbol): string { if (typeof cell === "string") return cell; - return cell.text.replace("", `${symbols[currency]}${cell.price[currency]}`); + return cell.text.replace("", cell.price[currency]); } export function PricingComparisonTable({ currency }: { currency: Symbol }) { From bae9ee5e5ed0d6e3b2494a80a7c7fcca1cb0336c Mon Sep 17 00:00:00 2001 From: ArthurGamby Date: Thu, 2 Apr 2026 17:13:58 +0200 Subject: [PATCH 6/6] fix(site): fix mobile layout issues on pricing page - Add top margin to Pro card on mobile so POPULAR badge doesn't overlap the card above it - Make comparison table horizontally scrollable on mobile with min-width to prevent column squishing Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/pricing/pricing-calculator.tsx | 2 +- .../app/pricing/pricing-comparison-table.tsx | 4 +-- .../src/app/pricing/pricing-hero-plans.tsx | 25 ++++++------------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/apps/site/src/app/pricing/pricing-calculator.tsx b/apps/site/src/app/pricing/pricing-calculator.tsx index 16093eb1ab..e95d96eb6b 100644 --- a/apps/site/src/app/pricing/pricing-calculator.tsx +++ b/apps/site/src/app/pricing/pricing-calculator.tsx @@ -553,7 +553,7 @@ export function PricingCalculator({ currency }: { currency: Symbol }) { variant="default-weaker" onClick={() => setBillingCycle(cycle)} className={cn( - "rounded-square px-3 py-1.5 text-xs font-sans-display [font-variation-settings:'wght'_700] transition-colors", + "rounded-square whitespace-nowrap flex-1 px-4 py-1.5 text-xs font-sans-display [font-variation-settings:'wght'_700] transition-colors", active ? "bg-background-ppg-reverse-strong text-foreground-ppg-reverse hover:bg-background-ppg-reverse-strong hover:text-foreground-ppg-reverse" : "text-foreground-neutral-weaker", diff --git a/apps/site/src/app/pricing/pricing-comparison-table.tsx b/apps/site/src/app/pricing/pricing-comparison-table.tsx index 92bae1f65a..9f52e665f2 100644 --- a/apps/site/src/app/pricing/pricing-comparison-table.tsx +++ b/apps/site/src/app/pricing/pricing-comparison-table.tsx @@ -18,8 +18,8 @@ function renderCell(cell: ComparisonCell, currency: Symbol): string { export function PricingComparisonTable({ currency }: { currency: Symbol }) { return ( -
- +
+
diff --git a/apps/site/src/app/pricing/pricing-hero-plans.tsx b/apps/site/src/app/pricing/pricing-hero-plans.tsx index 4517a2d7fe..50aa7ebf45 100644 --- a/apps/site/src/app/pricing/pricing-hero-plans.tsx +++ b/apps/site/src/app/pricing/pricing-hero-plans.tsx @@ -63,10 +63,7 @@ export function PricingHeroPlans({
- onCurrencyChange(value as Symbol)}> @@ -89,7 +86,7 @@ export function PricingHeroPlans({ key={planKey} className={`relative rounded-2xl border ${ highlighted - ? "border-stroke-ppg" + ? "border-stroke-ppg mt-4 md:mt-0" : "border-stroke-neutral-weak" } bg-background-default p-5 text-foreground-neutral shadow-[0px_18px_42px_0px_rgba(23,43,77,0.08)]`} > @@ -125,15 +122,10 @@ export function PricingHeroPlans({ )}
-

- {plan.subtitle} -

+

{plan.subtitle}

{plan.price[currency]} - - {" "} - / month - + / month