Payments and Subscriptions
The starter includes subscription checkout, a customer billing portal, and a webhook handler that saves Stripe subscription state in Supabase. This guide configures your app's subscriptions, not the one-time purchase of Ghoststack itself.
Before starting: complete authentication and database setup, sign in to your local app, and create a Stripe account.
Step 1: Start in a Stripe sandbox
Select a sandbox/test environment in Stripe. Add its test keys to .env.local using the names in .env.example:
STRIPE_SECRET_KEY=sk_test_YOUR_KEYThe starter redirects to Stripe-hosted Checkout and does not require a publishable Stripe key. Enter the secret key yourself. Restart the app after changing settings. Use the same Stripe account and environment for the keys, prices, portal, and webhook listener.
Test payments do not move money. Use Stripe's test payment details; a coupon in live mode is not a substitute for a sandbox. Official Stripe testing guide.
Step 2: Create your subscription plans
In the Stripe sandbox, create a product and a recurring monthly price for each paid plan. Copy its Price ID (price_...) into lib/plans.ts and update the displayed name, price, and features to match. The current UI assumes monthly USD prices. For annual billing or another currency, ask your agent to update the price display and checkout together.
lib/plans.ts supplies both /pricing and the landing page pricing section. Displaying a price does not create a Stripe price; they must match.
You can do this in Stripe's product catalog or with an optional Stripe MCP connection:
Use Stripe MCP to confirm the connected account and sandbox environment.
Create a product named [PLAN NAME] with a monthly recurring price of [AMOUNT]
in [CURRENCY]. Update lib/plans.ts with its Price ID and matching display details.
Use the existing plan structure and leave unrelated products unchanged.Step 3: Enable the customer portal
In the same Stripe sandbox, open the billing/customer portal settings and configure the actions you want customers to use, such as cancellation. Save the configuration. See Stripe portal setup.
Step 4: Forward webhooks to localhost
A webhook is Stripe's message to your app that a subscription changed. The browser redirect after checkout is not proof that the database was updated.
Install the Stripe CLI for your operating system. In a second terminal, authenticate and start the listener:
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripeUse the intended sandbox/test account. Do not add --live while developing. If your app uses another port, change 3000 in the command.
Copy the listener's whsec_... signing secret into .env.local as STRIPE_WEBHOOK_SECRET, then restart npm run dev. Keep both the app and listener running. This local secret is different from a deployed endpoint's signing secret.
Step 5: Test the complete flow
- Sign in, open
/pricing, and choose a paid plan. - In the test checkout, use
4242 4242 4242 4242, a future expiry date, and any three-digit CVC. - Finish checkout and confirm the redirect returns to your app.
- Check the Stripe CLI output for a successful webhook response and Supabase's
subscriptionstable for your user. - Open
/dashboard/settings: the correct plan should appear. - Open Manage in account settings, cancel, and confirm the subscription record updates. If cancellation is at period end, access should follow your product's chosen end-of-period policy.
- Test a declined payment using Stripe's test scenarios; access must not be granted just because a user reached a success URL.
Use an actual checkout in your app for this test. A generic stripe trigger event may lack your app's user metadata and cannot prove the complete flow works.
Restrict a feature to paying users
Billing setup does not automatically gate every feature you add. Use this prompt:
Restrict [FEATURE] to users with an entitled subscription.
Read the existing subscription helpers and AGENTS.md. Define the allowed statuses
and cancellation behavior for this feature. Enforce access in the server/API route
as well as showing an upgrade prompt in the UI. Test free, paid, canceled, and
logged-out users; do not trust a client-supplied subscription status.Production webhook setup
When the sandbox flow works and the app is deployed:
- Complete Stripe's account activation for live payments.
- Create the intended live products/prices; test Price IDs do not work with live keys. Update the deployed plan configuration.
- Set the live keys and the canonical HTTPS
NEXT_PUBLIC_APP_URLin your host's environment settings. - In Stripe's live webhook/event-destination settings, add
https://YOUR_DOMAIN/api/webhooks/stripe. - Subscribe to
checkout.session.completed,customer.subscription.updated, andcustomer.subscription.deleted, matching the starter handler. If you extend event handling, update this list too. - Copy that endpoint's signing secret into the host's
STRIPE_WEBHOOK_SECRETand redeploy. - Configure the customer portal in live mode too. Confirm the deployed endpoint is reachable and inspect webhook delivery failures in Stripe.
Keep development on test keys. The CLI listener is for local work; your deployed app needs the live webhook destination. Follow the pre-launch checklist before inviting paying customers.
If checkout succeeds but the plan does not appear
Check the webhook delivery first: correct mode, correct URL, listener running, matching secret, and a successful response. Then verify that the starter SQL has been applied and the Supabase server credentials are configured. Ask the agent to inspect logs with secrets and customer data redacted.