Skip to content
Zutra HyperSaaS — 6 languages, auth UI, dashboard, 69 tests
Get it on Gumroad

Zutra v1.3.0 — 6 languages, auth UI, dashboard, 69 tests. Saves 80+ hours.

See what's included

Billing Setup

Wire Stripe to the Zutra pricing page — products, prices, and the customer portal.

Overview

Zutra uses Stripe Payment Links as the integration model — no SDK, no server-side session management. Each plan in src/config/pricing.ts has a ctaHref that points to a Stripe Payment Link URL. Click “Start 14-day trial” on the Pro card → user lands on Stripe’s hosted checkout → comes back to /success after paying.

This is intentional: Payment Links let you launch billing in 5 minutes without writing webhook handlers or managing subscription state. When you outgrow it (custom trials, metered usage, dunning flows), replace the ctaHref with a call to your own /api/checkout route.

1. Get your Stripe keys

In Stripe Dashboard → Developers → API keys:

# .env
PUBLIC_STRIPE_KEY=pk_live_...        # safe to expose to the browser
STRIPE_SECRET_KEY=sk_live_...        # server-side only — never PUBLIC_

Use pk_test_ and sk_test_ keys during development.

2. Create products and prices

In Stripe Dashboard → Products:

  1. Click Add product
  2. Name it (e.g. “Pro Plan”)
  3. Add two recurring prices:
    • One monthly (e.g. $49/month)
    • One annual (e.g. $39/month, billed yearly)
  4. Toggle Free trial period to 14 days if you want a trial
  5. Save

Repeat for each plan in src/config/pricing.ts (Starter is typically free, so it skips Stripe).

For each paid product:

  1. In the product page, click Create payment link
  2. Configure:
    • Quantity: 1
    • Collect billing address: yes (required for tax)
    • After payment → redirect to https://yourdomain.com/success?plan=pro
  3. Click Create link
  4. Copy the URL — it starts with https://buy.stripe.com/...

4. Wire plan CTAs

Add the Payment Link URL to .env:

PUBLIC_STRIPE_PRO_LINK=https://buy.stripe.com/xxxxxxx

Then in src/config/pricing.ts, the Pro plan already references this variable:

{
  id:      "pro",
  cta:     "Start 14-day trial",
  ctaHref: import.meta.env.PUBLIC_STRIPE_PRO_LINK ?? `${SITE.appUrl}/signup?plan=pro`,
}

The ?? fallback means the build works even if the env var is missing — the button just sends users to your signup page instead of Stripe.

To add Payment Links for other plans:

PUBLIC_STRIPE_STARTER_LINK=https://buy.stripe.com/xxxxxxx
PUBLIC_STRIPE_ENTERPRISE_LINK=https://buy.stripe.com/xxxxxxx

Then add matching fields to Plan interface in src/config/pricing.ts and reference them in each plan’s ctaHref.

5. The /success page

src/pages/success.astro renders after Stripe redirects back. It uses the SuccessReceipt component which:

  • Reads the ?plan=pro query param to show which plan was bought
  • Displays an order ID, billing date, and “Go to Dashboard” CTA
  • Renders correctly even if the user never paid (it’s just a static receipt UI)

To make it dynamic (show real receipt data), pass the Stripe session_id query param to your backend, fetch the session via the Stripe API, and render the real values.

6. Customer Portal

For self-serve subscription management (cancel, update card, download invoices):

  1. In Stripe Dashboard → Settings → Billing → Customer Portal, click Activate
  2. Configure the branding and policies you want
  3. Copy the portal URL — it starts with https://billing.stripe.com/p/login/...
  4. Add to .env:
PUBLIC_STRIPE_PORTAL_URL=https://billing.stripe.com/p/login/xxxxxxx
  1. Wire it to your “Manage billing” button — typically in src/pages/dashboard/settings.astro or a billing settings sub-page.

7. Webhooks (optional, for advanced flows)

If you eventually need to react to subscription lifecycle events (renewal, cancellation, payment failure) — for example to flip a subscription_status column in your database — add a webhook endpoint:

  1. In Stripe Dashboard → Developers → Webhooks → Add endpoint
  2. URL: https://yourdomain.com/api/webhooks/stripe
  3. Events to listen for:
    • checkout.session.completed
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_failed
  4. Copy the signing secret to .env:
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxx
  1. Create src/pages/api/webhooks/stripe.ts to verify and route events. The project doesn’t ship this file — it’s intentionally left to you when you need it. A minimal handler:
import type { APIRoute } from 'astro';
import Stripe from 'stripe';

const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY);

export const POST: APIRoute = async ({ request }) => {
  const sig = request.headers.get('stripe-signature')!;
  const body = await request.text();
  const event = stripe.webhooks.constructEvent(
    body,
    sig,
    import.meta.env.STRIPE_WEBHOOK_SECRET
  );

  // handle event.type ...

  return new Response(null, { status: 200 });
};

Production checklist

  • Stripe live keys (pk_live_, sk_live_) set in production env
  • Payment Link URLs set for all paid plans
  • after_payment redirect on each Payment Link points to production /success?plan=...
  • Customer Portal activated and URL set
  • Webhook endpoint registered (only if you need server-side events)

Next steps