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
- A visitor clicks a plan on
/pricing. The button posts the plan's Stripe product id to/api/stripe/checkout. - 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
/signupfirst. - After payment, Stripe returns to
/api/stripe/checkout, which saves the customer, subscription and plan on the team and opens the dashboard. - The subscription is managed from the Team page, which opens the Stripe Customer Portal.
- Stripe sends
customer.subscription.updatedandcustomer.subscription.deletedto/api/stripe/webhook, which updates the team.
Set up products
Create the product in Stripe
Add a product with a default recurring price in the Stripe Dashboard.
Copy its id
Copy the product id. It starts with prod_.
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:webhookCopy 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.