Skip to content

Billing Setup

NuxtBase ships with a Stripe-based subscription flow for the example SaaS inside the template.

At the code level, the template expects:

  • a Stripe secret key
  • a Stripe publishable key
  • a Stripe webhook secret
  • four recurring Stripe Price IDs

Those Price IDs map the template’s paid sample plans:

  • pro monthly
  • pro yearly
  • plus monthly
  • plus yearly

Stripe now recommends Sandboxes for testing. They are the preferred testing environment over the older test mode.

Use a sandbox while you build and verify your integration:

  • sandbox API keys do not move real money
  • sandbox products, prices, customers, and subscriptions are isolated from live mode
  • sandbox settings are better isolated than the older shared test-mode behavior
  • sandbox works with the same test card numbers and webhook testing flows you already expect

For this setup page, treat sandbox as your default development environment. Do not start with live mode.

Terminal window
NUXT_STRIPE_SECRET_KEY=sk_test_...
NUXT_STRIPE_WEBHOOK_SECRET=whsec_...
NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NUXT_STRIPE_PRO_MONTHLY_PRICE_ID=price_...
NUXT_STRIPE_PRO_YEARLY_PRICE_ID=price_...
NUXT_STRIPE_PLUS_MONTHLY_PRICE_ID=price_...
NUXT_STRIPE_PLUS_YEARLY_PRICE_ID=price_...

Without the Price IDs, checkout cannot resolve which Stripe price belongs to which plan and interval.

In sandbox, these values usually look like:

  • NUXT_STRIPE_SECRET_KEY=sk_test_...
  • NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

In production, you must switch to the live equivalents:

  • NUXT_STRIPE_SECRET_KEY=sk_live_...
  • NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...

If you are not enabling Stripe yet, do not leave the copied placeholder key in .env.

After copying .env.example, remove or blank out the billing values:

Terminal window
# NUXT_STRIPE_SECRET_KEY=
# NUXT_STRIPE_WEBHOOK_SECRET=
# NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
# NUXT_STRIPE_PRO_MONTHLY_PRICE_ID=
# NUXT_STRIPE_PRO_YEARLY_PRICE_ID=
# NUXT_STRIPE_PLUS_MONTHLY_PRICE_ID=
# NUXT_STRIPE_PLUS_YEARLY_PRICE_ID=

This matters because the billing service checks whether NUXT_STRIPE_SECRET_KEY is non-empty. A fake placeholder like sk_test_... is still non-empty, so the template will try to initialize Stripe instead of using the mock fallback.

The mapping is static in the template source:

  • pro -> stripeProMonthlyPriceId / stripeProYearlyPriceId
  • plus -> stripePlusMonthlyPriceId / stripePlusYearlyPriceId

That means your Stripe dashboard must contain four recurring prices that match those sample plans.

Step 1: Create Products and Prices in a Sandbox

Section titled “Step 1: Create Products and Prices in a Sandbox”

Inside your Stripe sandbox, create your paid plans and add recurring prices for:

  1. pro monthly
  2. pro yearly
  3. plus monthly
  4. plus yearly

Then copy each price_... ID into the matching environment variable.

Stripe CLI is the standard tool for local webhook testing.

Official install guide:

On macOS with Homebrew:

Terminal window
brew install stripe/stripe-cli/stripe

Then authenticate:

Terminal window
stripe login
Terminal window
pnpm dev

NuxtBase verifies Stripe signatures from the raw request body, so webhook forwarding must be correct.

Use Stripe CLI:

Terminal window
stripe listen --forward-to localhost:3000/api/billing/webhook

Stripe CLI prints a temporary webhook secret. Copy that value into:

Terminal window
NUXT_STRIPE_WEBHOOK_SECRET=whsec_...

If you restart stripe listen, you usually get a new secret. Update .env again before testing more webhook flows.

Step 5: Test the Checkout Flow in a Sandbox

Section titled “Step 5: Test the Checkout Flow in a Sandbox”

Stripe’s official testing environments support mock payment behavior without real charges.

Use a sandbox card for a basic successful payment:

4242 4242 4242 4242

Stripe also provides additional test card numbers and test scenarios for:

  • card declines
  • authentication flows such as 3DS
  • insufficient funds
  • other common payment outcomes

Official testing docs:

The intended local test path is:

  1. register a user
  2. open the dashboard billing area
  3. start checkout for a paid plan
  4. complete the Stripe test checkout
  5. confirm Stripe sends the webhook
  6. verify subscription data appears back in the app

The return URLs in the template go back to the billing page with query params such as:

/dashboard/billing?checkout=success
/dashboard/billing?checkout=canceled

If you want to test recurring billing behavior over time, Stripe also recommends using sandbox tools such as test clocks where applicable.

If NUXT_STRIPE_SECRET_KEY is blank or removed, the billing service falls back to mock checkout and mock portal URLs.

Leaving the placeholder sk_test_... in .env does not count as “missing”.

That is useful for:

  • exploring the UI
  • avoiding hard blockers on day one

But it is not enough for validating:

  • real subscription creation
  • webhook processing
  • Stripe customer portal behavior
  • billing event logs

For real billing verification, you need a real Stripe sandbox account and real Stripe test objects inside that sandbox.

After your sandbox flow works, switch to production deliberately.

That means:

  1. use live API keys instead of sandbox keys
  2. create or verify the live products and four live recurring prices
  3. register a real production webhook endpoint in Stripe
  4. replace the local CLI webhook secret with the production endpoint secret
  5. redeploy the app with live billing configuration
  6. run a real production smoke test yourself

Your production smoke test should confirm:

  • checkout opens from the real app
  • payment completes with your real production configuration
  • Stripe sends the production webhook successfully
  • the app reflects the subscription correctly after the webhook
  • customer portal and billing pages still work

Stripe currently has both API v1 and API v2 namespaces.

For this billing setup page, the important distinction is:

  • the common billing resources used here, such as Products, Prices, Checkout Sessions, Subscriptions, Customers, and most webhook flows, are still documented in Stripe’s established API v1 namespace
  • Stripe API v2 exists and Stripe supports using v1 and v2 in the same integration, but v2 follows different request and event patterns

The practical guidance for NuxtBase is:

  • keep this billing setup aligned with the Stripe Billing resources the template already uses
  • prefer the latest official Stripe SDK and Stripe CLI versions
  • prefer the latest Stripe API version that your account and integration are ready to support
  • if you intentionally adopt newer Stripe features that depend on v2-specific behavior, review the v2 docs separately before changing your integration assumptions

Official versioning docs:

Use this checklist before you trust the integration:

  • you are building in a Stripe sandbox, not live mode
  • NUXT_STRIPE_SECRET_KEY is a sandbox or live key from the correct Stripe account
  • NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY matches the same account
  • all four price_... IDs exist and are recurring prices
  • sandbox test cards work for the expected payment scenarios
  • Stripe CLI forwards events to /api/billing/webhook
  • NUXT_STRIPE_WEBHOOK_SECRET matches the current CLI session or production endpoint
  • production uses separate live keys, live prices, and a live webhook endpoint
  • you have personally tested the production billing flow after deployment

Stripe changes quickly, so keep the official docs close while configuring billing: