From bb526b7ba9dc59d9e2d579e1758b086866ea2188 Mon Sep 17 00:00:00 2001 From: Rae McKelvey <633012+okdistribute@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:56:48 -0700 Subject: [PATCH 1/5] Add pricing more prominently --- src/app/pricing/page.jsx | 275 +++++++++++++++++++++++++++ src/components/Header.jsx | 2 +- src/components/PricingCalculator.jsx | 183 ++++++++++++++++++ 3 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 src/app/pricing/page.jsx create mode 100644 src/components/PricingCalculator.jsx diff --git a/src/app/pricing/page.jsx b/src/app/pricing/page.jsx new file mode 100644 index 00000000..7d042212 --- /dev/null +++ b/src/app/pricing/page.jsx @@ -0,0 +1,275 @@ +import { Fragment } from 'react' +import { HeaderSparse } from '@/components/HeaderSparse' +import { FooterMarketing } from '@/components/FooterMarketing' +import { PricingCalculator } from '@/components/PricingCalculator' +import Link from 'next/link' +import { Check, Minus, Activity, BarChart3, Server, Headphones } from 'lucide-react' + +export const metadata = { + title: 'Pricing | Iroh', + description: 'Plans and pricing for iroh services. Free for development, Pro for production, Enterprise for large-scale deployments.', +} + +const SERVICES_URL = 'https://services.iroh.computer' +const CAL_URL = 'https://cal.com/team/number-0/iroh-services' + +const plans = [ + { + name: 'Free', + description: 'For local development and testing.', + price: '$0', + period: '/month', + href: `${SERVICES_URL}?utm_source=website&utm_content=pricing-free`, + buttonLabel: 'Get Started', + features: [ + 'All features in Pro, limited', + '1 day retention', + 'Community support', + ], + }, + { + name: 'Pro', + description: 'For shipping your app to prod.', + price: '$19', + period: '/month', + popular: true, + href: `${SERVICES_URL}?utm_source=website&utm_content=pricing-pro`, + buttonLabel: 'Free trial', + features: [ + 'Pay as you go pricing', + '7 day retention', + '8x5 support tickets', + ], + }, + { + name: 'Enterprise', + description: 'For large-scale deployments.', + price: 'Contact Us', + period: '', + href: CAL_URL, + buttonLabel: "Let's Chat", + features: [ + 'On-prem and multi-cloud', + 'Custom retention', + 'SLAs', + 'Support Engineer', + ], + }, +] + +const sectionIcons = { + Platform: Activity, + Metrics: BarChart3, + Hosting: Server, + Support: Headphones, +} + +const featureSections = [ + { + name: 'Platform', + features: [ + { name: 'Collaborators', free: 'One user', pro: 'Unlimited users', enterprise: 'Unlimited users' }, + { name: 'Network Diagnostics', free: '3 reports', pro: '10 reports', proNote: 'then pay as you go', enterprise: 'Custom', enterpriseNote: 'Volume discounts' }, + ], + }, + { + name: 'Metrics', + features: [ + { name: 'Data Points per Minute', free: '1K DPM', freeNote: '100 metrics \u00d7 10 endpoints', pro: '10K DPM', proNote: 'then $1.49/1K DPM', enterprise: 'Custom', enterpriseNote: 'Volume discounts' }, + { name: 'Retention', free: '1 day', pro: '7 days', enterprise: 'Custom' }, + { name: 'Concurrent Endpoints', free: '10', pro: '100', proNote: 'then $0.50/100 endpoints', enterprise: 'Custom' }, + ], + }, + { + name: 'Hosting', + features: [ + { name: 'Relays', free: 'Public', freeNote: 'Multi-tenant', pro: 'Cloud', proNote: '$199/relay', enterprise: 'Custom' }, + { name: 'Connections per Relay', free: 'Variable', freeNote: 'Rate-limited', pro: '60k', enterprise: 'Custom' }, + { name: 'Multi-region', free: null, pro: true, enterprise: 'Custom' }, + { name: 'Multi-cloud', free: null, pro: null, enterprise: true }, + { name: 'Dedicated DNS', free: null, pro: null, enterprise: true }, + ], + }, + { + name: 'Support', + features: [ + { name: 'Community (Github & Discord)', free: true, pro: true, enterprise: true }, + { name: 'SLAs', free: null, pro: null, enterprise: 'Custom' }, + { name: 'Dedicated Support Engineer', free: null, pro: null, enterprise: true }, + ], + }, +] + +function FeatureCell({ value, note }) { + if (value === true) { + return ( + + + + ) + } + if (value === null || value === undefined) { + return ( + + + + ) + } + return ( + +
{value}
+ {note &&

{note}

} + + ) +} + +export default function PricingPage() { + return ( +
+ + +
+ {/* Hero */} +
+
+

Plans and Pricing

+

+ Go to production with confidence. +

+

+ Customized hosting and monitoring for iroh apps, with people that support you. +

+
+
+ + {/* Plan Cards */} +
+
+
+ {plans.map((plan) => ( +
+ {plan.popular && ( + + Most popular + + )} +

{plan.name}

+

+ {plan.description} +

+

+ {plan.price} + {plan.period && ( + + {plan.period} + + )} +

+
+
+
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+ + {plan.buttonLabel} + +
+ ))} +
+
+
+ + {/* Feature Comparison Grid */} +
+
+

Compare plans

+
+ + + + + + + + + + {featureSections.map((section) => { + const Icon = sectionIcons[section.name] + return ( + + + + + {section.features.map((feature) => ( + + + + + + + ))} + + ) + })} + +
+ FreeProEnterprise
+ + {Icon && } + {section.name} + +
+ {feature.name} +
+
+
+
+ + {/* Calculator */} +
+
+ +
+
+ + {/* CTA */} +
+
+

+ Need hosting, support or development?{' '} + + Book a meeting + +

+
+
+
+ + +
+ ) +} + diff --git a/src/components/Header.jsx b/src/components/Header.jsx index a273f2f7..973fb623 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -30,7 +30,7 @@ export const navItems = [ ]}, {content: 'Docs', href: 'https://docs.iroh.computer/'}, {content: 'Blog', href: '/blog'}, - {content: 'Roadmap', href: '/roadmap'}, + {content: 'Pricing', href: '/pricing'}, ]; export function TopLevelNavItem({href, children}) { diff --git a/src/components/PricingCalculator.jsx b/src/components/PricingCalculator.jsx new file mode 100644 index 00000000..e3163021 --- /dev/null +++ b/src/components/PricingCalculator.jsx @@ -0,0 +1,183 @@ +'use client' + +import { useState } from 'react' + +const PRO_BASE = 19 +const INCLUDED_ENDPOINTS = 100 +const ENDPOINT_RATE = 0.50 +const METRICS_RATE = 1.49 +const RELAY_RATE = 199 + +const relayOptions = [0, 1, 2, 3, 4, 5] +const peakConnectionOptions = [100, 200, 300, 500, 1000, 2000, 5000, 10000] +const avgConnectionOptions = [1, 10, 50, 100, 250, 500, 1000, 2000, 5000] +const metricsPerNodeOptions = [87, 90, 100, 110] + +const frequencyOptions = [ + { label: 'Every minute', value: '1', factor: 1 }, + { label: 'Every 5 minutes', value: '0.2', factor: 0.2 }, + { label: 'Every hour', value: '0.0167', factor: 1 / 60 }, + { label: 'Every day', value: '0.0007', factor: 1 / 1440 }, +] + +function formatPrice(n) { + return `$${n.toFixed(2)}` +} + +function formatNumber(n) { + return n.toLocaleString() +} + +function SelectInput({ label, description, value, onChange, options, formatOption }) { + return ( +
+ +

{description}

+ +
+ ) +} + +export function PricingCalculator() { + const [relays, setRelays] = useState(1) + const [peakConnections, setPeakConnections] = useState(500) + const [avgConnections, setAvgConnections] = useState(100) + const [metricsPerNode, setMetricsPerNode] = useState(87) + const [frequency, setFrequency] = useState('1') + + const freq = frequencyOptions.find((f) => f.value === frequency)?.factor ?? 1 + const dpm = avgConnections * metricsPerNode * freq + + const extraConnections = Math.max(0, peakConnections - INCLUDED_ENDPOINTS) + const connectionsCost = (extraConnections / 100) * ENDPOINT_RATE + const metricsCost = (dpm / 1000) * METRICS_RATE + const relayCost = relays * RELAY_RATE + const total = PRO_BASE + connectionsCost + metricsCost + relayCost + + return ( +
+

Estimate your monthly cost

+

+ Adjust the values to see what Pro would cost for your workload. +

+
+
+ {/* Inputs */} +
+ + + + +
+ +

+ How often each endpoint pushes metrics +

+ +
+

+ Calculated DPM:{' '} + + {formatNumber(Math.round(dpm))} + +

+
+ + {/* Cost breakdown */} +
+

Cost breakdown

+
+
+ Pro plan base + {formatPrice(PRO_BASE)}/mo +
+ +
+
+ Relays + {formatPrice(relayCost)}/mo +
+

+ {relays} × ${RELAY_RATE}/each +

+
+ +
+
+ Connections + {formatPrice(connectionsCost)}/mo +
+

+ {extraConnections === 0 + ? 'Included in base plan' + : `${formatNumber(extraConnections)} extra \u00d7 $${ENDPOINT_RATE}/100`} +

+
+ +
+
+ Metrics DPM + {formatPrice(metricsCost)}/mo +
+

+ {formatNumber(Math.round(dpm))} DPM × ${METRICS_RATE}/1K +

+
+ +
+
+ Estimated total + {formatPrice(total)}/mo +
+
+
+
+
+
+
+ ) +} From 03560b1f53df72f5f9612b11cbe814e54f058d5e Mon Sep 17 00:00:00 2001 From: Rae McKelvey <633012+okdistribute@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:10:10 -0700 Subject: [PATCH 2/5] Fix light mode nav text visibility and default to light theme Nav links and dropdown items were using grey text colors that were hard to read in light mode. Changed to black text for light mode while preserving dark mode colors. Set light as the default theme. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/providers.jsx | 2 +- src/components/Header.jsx | 2 +- src/components/HeaderSparse.jsx | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/providers.jsx b/src/app/providers.jsx index fd430207..bf4ae474 100644 --- a/src/app/providers.jsx +++ b/src/app/providers.jsx @@ -31,7 +31,7 @@ export const AppContext = createContext({}) export function Providers({ children }) { return ( - + {children} diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 973fb623..8a15bcf5 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -77,7 +77,7 @@ function DropdownNavItem({ label, items }) { {item.label} diff --git a/src/components/HeaderSparse.jsx b/src/components/HeaderSparse.jsx index bb771b67..23cf4174 100644 --- a/src/components/HeaderSparse.jsx +++ b/src/components/HeaderSparse.jsx @@ -12,7 +12,7 @@ function TopLevelNavItem({ href, children }) {
  • {children} @@ -38,7 +38,7 @@ function DropdownNavItem({ href, label, items }) {
  • diff --git a/src/components/HeaderSparse.jsx b/src/components/HeaderSparse.jsx index 23cf4174..f34f53fc 100644 --- a/src/components/HeaderSparse.jsx +++ b/src/components/HeaderSparse.jsx @@ -85,9 +85,9 @@ export function HeaderSparse() { function handleScroll() { const navbar = document.getElementById('navbar'); if (window.scrollY > 0) { - navbar.classList.add('bg-irohGray-50', 'shadow-md', 'dark:bg-irohGray-900'); + navbar.classList.add('bg-white', 'shadow-md', 'dark:bg-irohGray-900'); } else { - navbar?.classList.remove('bg-irohGray-50', 'shadow-md', 'dark:bg-irohGray-900'); + navbar?.classList.remove('bg-white', 'shadow-md', 'dark:bg-irohGray-900'); } } @@ -100,7 +100,7 @@ export function HeaderSparse() { return (