Skip to content
Browse documentation
On this page

Payments

Connect Stripe products to the pricing page, handle subscriptions and gate paid features.

Billing is per team. Stripe Checkout starts a subscription, the Customer Portal manages it, and a webhook keeps the team's plan up to date.

How a purchase flows

  1. A visitor clicks a plan on /pricing. The button posts the plan's Stripe product id to /api/stripe/checkout.
  2. The route checks the product is active, takes its default price and starts a Checkout session. Visitors who aren't signed in are sent to /signup first.
  3. After payment, Stripe returns to /api/stripe/checkout, which saves the customer, subscription and plan on the team and opens the dashboard.
  4. The subscription is managed from the Team page, which opens the Stripe Customer Portal.
  5. Stripe sends customer.subscription.updated and customer.subscription.deleted to /api/stripe/webhook, which updates the team.

Set up products

  1. Create the product in Stripe

    Add a product with a default recurring price in the Stripe Dashboard.

  2. Copy its id

    Copy the product id. It starts with prod_.

  3. Link it to a plan

    In the CMS, open the Pricing page, edit the plan in the Pricing block and paste the id into its Stripe product field. Save and publish.

A plan without a product id keeps a normal link button, which suits a free tier or a "contact us" plan.

Gating paid features

The team's billing state is stored as JSON in organization.metadata: stripeCustomerId, stripeSubscriptionId, stripeProductId, planName and subscriptionStatus.

import { getOrganizationForUser } from '@/db/queries';

const org = await getOrganizationForUser(user.id, Astro.locals.organizationId);
const isPaid = ['active', 'trialing'].includes(org?.meta?.subscriptionStatus);
if (!isPaid) return Astro.redirect('/pricing');

Compare stripeProductId rather than planName when gating by plan: names can be edited in Stripe, ids can't. Gate on the server.

Webhooks

In the Stripe Dashboard, add an endpoint at https://yourdomain.com/api/stripe/webhook and subscribe to customer.subscription.updated and customer.subscription.deleted. Put its signing secret in the STRIPE_WEBHOOK_SECRET secret.

Testing

Forward webhooks to your machine while you develop:

bun run stripe:webhook

Copy the whsec_… value it prints into .dev.vars as STRIPE_WEBHOOK_SECRET. Pay with the test card 4242 4242 4242 4242, any future expiry date and any three-digit CVC.