Payments
Dune's payment module wires Stripe checkout, webhook handling, and customer portal into your site with minimal configuration. When a user completes a purchase, Dune automatically assigns the configured role to their site user account.
Payments require public site authentication to be configured — the payment flow reads the current SiteUser from the session.
Configuration
# site.yaml
payments:
provider: "stripe"
secret_key: "$STRIPE_SECRET_KEY"
webhook_secret: "$STRIPE_WEBHOOK_SECRET"
products:
- id: "membership"
name: "Monthly Membership"
price_id: "price_1Abc123" # Stripe Price ID from your dashboard
role: "member" # Dune role assigned after payment
mode: "subscription" # "subscription" | "payment"
- id: "lifetime"
name: "Lifetime Access"
price_id: "price_1Xyz789"
role: "member"
mode: "payment"
secret_key and webhook_secret support "$ENV_VAR" expansion — keep them out of committed config.
Routes
When payments: is configured, three routes are registered:
| Route | Description |
|---|---|
POST /payments/checkout/:productId |
Create a Stripe Checkout session and redirect to it |
POST /payments/webhook |
Receive Stripe webhook events (must be registered in Stripe dashboard) |
GET /payments/portal |
Redirect to Stripe Customer Portal for subscription management |
The checkout and portal routes require a logged-in SiteUser. Unauthenticated requests get a 401.
Checkout flow
- User clicks a "Subscribe" button that POSTs to
/payments/checkout/membership - Dune creates a Stripe Checkout Session with the configured price and
success_url/cancel_urlpointing back to your site - Dune returns
303 See Otherto the Stripe-hosted checkout page - User completes payment on Stripe
- Stripe sends a
checkout.session.completedevent to/payments/webhook - Dune verifies the webhook signature, finds or creates the
SiteUser, and assigns the configuredrole
Webhook setup
Register your webhook in the Stripe dashboard:
- Endpoint URL:
https://your-site.com/payments/webhook - Events to listen for:
checkout.session.completed,customer.subscription.deleted
Copy the webhook signing secret (whsec_…) to STRIPE_WEBHOOK_SECRET.
Dune verifies the Stripe-Signature header on every webhook request using HMAC-SHA256. Requests with an invalid or missing signature are rejected with 400.
Role assignment
After a successful checkout.session.completed event, Dune adds the product's role to the SiteUser.roles array. This role can be used in content gating rules:
# In content frontmatter:
roles: member
For subscription mode products, Dune also handles customer.subscription.deleted — when a subscription is cancelled, the role is removed from the user.
Customer portal
GET /payments/portal creates a Stripe Billing Portal session and redirects the user to it. The portal lets users manage their subscription, update payment methods, and view invoices.
Requires a logged-in user. The portal is pre-configured for the customer ID stored on the user's record after their first purchase.
@dune/core/ui — SubscriptionForm
Use the built-in SubscriptionForm component to trigger checkout:
import { SubscriptionForm } from "@dune/core/ui";
export default function PricingPage({ Layout, ...props }) {
return (
<Layout {...props}>
<h1>Become a member</h1>
<SubscriptionForm productId="membership" label="Subscribe — $10/month" />
</Layout>
);
}
The component POSTs to /payments/checkout/{productId} and shows a loading state while the redirect is in progress.
Testing with Stripe test mode
Use Stripe test keys (sk_test_…, whsec_… from the test mode dashboard) during development. The Stripe CLI can forward webhooks to your local server:
stripe listen --forward-to localhost:3000/payments/webhook
Use Stripe test card numbers (4242 4242 4242 4242) to simulate successful payments.