Overview

Pricing management lets your team instantly update and experiment on pricing, without redeploying your app.

Problem

Without pricing management:

  • Complex updates — Changing pricing requires coordinated code changes across the frontend, backend, and marketing site. Rolling back changes is even harder.

  • Engineering bottlenecks — Product and marketing depend on engineering for every pricing change.

  • Limited experimentation — It's difficult to test and iterate, leading to suboptimal pricing and lost revenue.

Solution

Define your pricing in Hypertune as flags instead of hardcoded values:

type Root {
  pricing: Pricing!
}

type Pricing {
  stripePrices(plan: Plan!): [StripePrice!]!
  planContent(plan: Plan!): PlanContent!
  planFeatures: [PlanFeature!]!
  planOrdering: [Plan!]!
}

enum Plan { free, pro, enterprise }

type StripePrice {
  id: String!
  type: StripePriceType!
}

enum StripePriceType { flatFee, perSeat }

type PlanContent {
  name: String!
  description: String!
  features: [String!]!
}

type PlanFeature {
  name: String!
  value(plan: Plan!): PlanFeatureValue!
}

type PlanFeatureValue {
  isIncluded: Boolean!
  text: String!
}

Then reference it in your code:

api/upgrade/route.ts
import { waitUntil } from '@vercel/functions'
import { NextResponse } from 'next/server'
import getHypertune from '@/lib/getHypertune'
import getStripe from '@/lib/getStripe'
import getTeamSize from '@/lib/getTeamSize'

export const runtime = 'nodejs'

export async function GET(request: Request) {
  const hypertune = await getHypertune({ isRouteHandler: true })

  const stripePrices = hypertune
    .pricing()
    .stripePrices({ args: { plan: 'pro' } })
    .map((price) => price.get())

  const teamSize = getTeamSize()

  const lineItems = stripePrices.map((price) => ({
    price: price.id,
    quantity: price.type === 'perSeat' ? teamSize : 1,
  }))

  const stripe = getStripe()

  const baseUrl = new URL(request.url).origin

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: lineItems,
    success_url: `${baseUrl}/success`,
    cancel_url: `${baseUrl}/cancel`,
  })

  waitUntil(hypertune.flushLogs())

  return NextResponse.json({
    sessionUrl: session.url,
    sessionId: session.id,
  })
}
components/Pricing.tsx
import FeaturesTable from './FeaturesTable'
import PlansTable from './PlansTable'
import getHypertune from '@/lib/getHypertune'

export default async function Pricing() {
  const hypertune = await getHypertune()
  const pricing = hypertune.pricing()

  return (
    <div className="pricing">
      <h2>Pricing</h2>
      <PlansTable pricing={pricing} />
      <FeaturesTable pricing={pricing} />
    </div>
  )
}
components/PlansTable.tsx
import {
  type PricingNode,
  type PlanContentNode,
} from '@/generated/hypertune'

export default function PlansTable({
  pricing,
}: {
  pricing: PricingNode
}) {
  const planOrdering = pricing.planOrdering({
    itemFallback: 'free',
  })

  return (
    <div className="plans-table">
      {planOrdering.map((plan) => {
        const planContent = pricing.planContent({
          args: { plan },
        })
        return (
          <PlanColumn key={plan} planContent={planContent} />
        )
      })}
    </div>
  )
}

function PlanColumn({
  planContent,
}: {
  planContent: PlanContentNode
}) {
  const content = planContent.get()

  return (
    <div className="plan-column">
      <h3>{content.name}</h3>
      <p>{content.description}</p>
      <ul>
        {content.features.map((feature) => (
          <li key={feature}>{feature}</li>
        ))}
      </ul>
    </div>
  )
}
components/FeaturesTable.tsx
import { type PricingNode } from '@/generated/hypertune'

export default function FeaturesTable({
  pricing,
}: {
  pricing: PricingNode
}) {
  const planOrdering = pricing.planOrdering({
    itemFallback: 'free',
  })
  const planFeatures = pricing.planFeatures()

  return (
    <table className="features-table">
      <thead>
        <tr>
          <th>Feature</th>
          {planOrdering.map((plan) => (
            <th key={plan}>{plan}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {planFeatures.map((feature) => {
          const featureName = feature.name({
            args: {},
            fallback: '',
          })
          return (
            <tr key={featureName}>
              <td>{featureName}</td>
              {planOrdering.map((plan) => {
                const value = feature
                  .value({ args: { plan } })
                  .get()
                return (
                  <td key={plan}>
                    {value.text ? (
                      <span className="text">{value.text}</span>
                    ) : value.isIncluded ? (
                      <span className="tick">✓</span>
                    ) : (
                      <span className="cross">✗</span>
                    )}
                  </td>
                )
              })}
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}

This empowers product and marketing to instantly update and experiment on pricing from the Hypertune dashboard without any code changes or redeploys:

Benefits

  • Instant updates — Adjust pricing across your frontend, backend, and marketing site without redeploying or coordinating code changes.

  • Empowered teams — Product and marketing can update pricing independently, without waiting on engineering.

  • Faster iteration — Tune pricing in real time to maximize revenue and growth.

ROI

These benefits help teams:

  • Ship more pricing improvements, faster, with the same headcount.

  • Improve key business metrics, e.g. revenue.

Last updated