Module 7 of 10

Taking payments

A video for this module is coming.

Shipyard adds subscriptions to your app with Stripe, a service that handles card payments. You build and test everything with fake cards first. Real money comes last, and only when you say so.

What you'll do

You will add the billing module to your app, set your plans, let Shipyard create the products and prices in Stripe's test mode, make a fake purchase, cancel it, and then (only if you want real payments) go live step by step.

This is not legal, tax or financial advice. Taxes, invoices and consumer rules differ by country. Check with an accountant before taking real payments. Prices and fees in this module are as of October 2026. Check them before relying on them.

Know these first

  • You are the seller. With Stripe, you handle VAT or sales tax, invoices and refunds yourself. Module 6 helps you find what applies in your country.
  • A merchant of record is the alternative. Paddle, Lemon Squeezy, Polar and Stripe Managed Payments become the seller and handle VAT and invoices for you. They charge about 3.5% to 5% plus fees. They suit you if you sell worldwide to consumers and do not want the tax work. The Shipyard billing module uses plain Stripe. If you choose a merchant of record, that part is not built yet, so stop at the pricing decision.
  • Free Vercel does not allow commercial use. Vercel's free Hobby plan is for non-commercial use, and taking payments counts as commercial. Going live needs Vercel Pro, $20 a month.
  • Stripe charges fees on each payment. They depend on your country and the card. Read Stripe's pricing page for your country.
  • Apple has its own rule. See the last section of this page.

Before you start

  • A Stripe account, left in Test mode (the switch at the top of the Stripe dashboard).
  • The test keys in your keys file: STRIPE_SECRET_KEY (starts with sk_test_) and STRIPE_PUBLISHABLE_KEY. Those slots only accept test keys. You paste them into the file yourself, never into the chat.
  • Your legal pages made after the billing module is added (module 6). They must mention Stripe, subscriptions and the 14-day right to withdraw. Payments also need a phone number for customers, or the answer "none".

Steps: test mode

Claude runs these commands for you inside Claude Code. They are shown so you know what is happening; they do not work if you paste them into a normal terminal.

  1. In Claude Code, run /shipyard:launch and choose "Payments in test mode".

  2. Claude previews the change, then adds the module:

    node "${CLAUDE_PLUGIN_ROOT}/scripts/add-module.mjs" <appDir> billing --dry-run
    node "${CLAUDE_PLUGIN_ROOT}/scripts/add-module.mjs" <appDir> billing
    

    It then creates the new database tables, installs packages and pushes the new settings to Vercel.

  3. Choose your plans. Claude asks what you sell. There is one free plan and one paid plan, priced in euro. Prices live in one place, src/lib/billing/plans.ts, between // shipyard:plans:start and // shipyard:plans:end. If you change a price later, a new Stripe price is created and existing subscribers keep the old one.

  4. Claude runs the checks (npm run typecheck, npm run lint, npm test), then sets up Stripe:

    node "${CLAUDE_PLUGIN_ROOT}/scripts/stripe-setup.mjs" <appDir>
    

    This creates the products, the prices, a customer portal (cancel, update card, invoices) and a webhook. A webhook is how Stripe tells your app that a payment happened. Its secret goes straight to Vercel and is never shown. It is safe to run again after editing plans.

  5. Do one thing yourself in the Stripe dashboard: Settings, Business, Public details, and add your Terms link (your-address/terms). Checkout shows a box where the buyer agrees to the terms and asks for the service to start now. Stripe refuses to show it until the link is saved.

  6. Save and publish the change through a branch and pull request, then deploy. Claude asks before saving to GitHub.

  7. Make a fake purchase together:

    • Sign in on the live address, open /pricing, press Upgrade.
    • On Stripe's page use card 4242 4242 4242 4242, any future date, any 3 digits, any name and postcode.
    • Back in the app, Settings should show the paid plan. That proves the webhook works.
    • Press "Manage billing" and cancel. It should say the plan ends on a date.
    • Buy again and try "Withdraw from contract" in Settings, Billing. The plan ends at once and an email confirms. You then refund what was paid minus the days used, in the Stripe dashboard.
    • Try the decline card 4000 0000 0000 0002, and the card that asks for a code, 4000 0025 0000 3155.
  8. Run the launch check: node "${CLAUDE_PLUGIN_ROOT}/scripts/launch-check.mjs" <appDir>. Open /admin/health and look for green on Stripe key mode (test), prices, webhook and portal.

Paid features are checked on the server with requirePaid(), never only in the page. Otherwise someone could unlock them by editing what their browser shows.

Steps: going live

Only start when you say "I want real payments". Do these in order. If one is not done, go back to it.

  1. Your own domain (module 5). Live Stripe wants a real website address, and the live check refuses vercel.app.
  2. Confirmed sign-ups with a verified sending domain (module 5).
  3. Legal pages made and read (module 6), and checked by a professional.
  4. The country checklist done. You have confirmed business registration and tax steps for your country, or knowingly postponed them with an accountant.
  5. A full test-mode purchase and cancel (above).
  6. Vercel Pro. You upgrade in the Vercel dashboard yourself, then tell Claude.
  7. Your Stripe live account. In the Stripe dashboard only, you finish the business details, verify your identity and add your bank account for payouts. This can take days. Claude never enters these for you. Then you create a live key and paste it into the keys file as STRIPE_LIVE_SECRET_KEY (and the publishable one as STRIPE_LIVE_PUBLISHABLE_KEY). Never in the chat.
  8. A clean test-mode run of launch-check.mjs, with no failures.
  9. Live setup: node "${CLAUDE_PLUGIN_ROOT}/scripts/stripe-setup.mjs" <appDir> --live. It refuses with a plain list if the domain, legal pages, confirmed sign-ups, Vercel Pro, test mode or the launch check is not done. It does not check the professional review or your country checklist, so those are on you. It creates live products, prices, portal and webhook, and puts the live keys in Vercel production only. Add the Terms link in Stripe while Test mode is off too, then redeploy.
  10. One real purchase. You buy the cheapest plan with your own card, check the paid state in the app, then refund yourself in the Stripe dashboard. The card fee is not always returned.
  11. The final gate: launch-check.mjs <appDir> --live. Fix every failure. Open /admin/health on the live site.
  12. Announce it. Then watch failed payments and refunds in Stripe and the support emails to your contact address.

Two rules: live keys are never switched on while checks are red, and if anything ever charges the wrong amount, pause the product in the Stripe dashboard first, then fix it.

Selling inside a phone app

Apple requires its own in-app purchase for digital subscriptions sold inside an iOS app, in most regions. Selling through your website or the phone browser is fine. Selling inside the store app is not. Do not put Stripe buttons for digital goods in an iOS app without checking Apple's current rules for your region. Google Play has its own payments policy too. More in module 8.

Done when

  • A test purchase, a cancel and a withdrawal all worked, and the app showed the right plan each time.
  • /admin/health shows Stripe in test mode with prices, webhook and portal green.
  • If you went live: one real purchase worked and you refunded it, and launch-check.mjs --live has no failures.

If something goes wrong

  • Checkout says the owner has to finish setting up. The Terms link is missing in Stripe's public details. Add it.
  • The plan does not change after a test purchase. The webhook is not reaching the app. Check that you redeployed after stripe-setup.mjs, and look at the webhook in the Stripe dashboard.
  • stripe-setup.mjs --live refuses. Read the list. It names each missing step.
  • A deployed price looks wrong. Edit only the block in plans.ts, run stripe-setup.mjs again and redeploy.

Check yourself

Why is the free Vercel plan not enough once you take money? It is for non-commercial use only, and payment checkout counts as commercial. You need Vercel Pro, $20 a month.
What is a merchant of record, and when might you pick one? A company that becomes the seller and handles VAT and invoices for a fee. It suits you if you sell worldwide to consumers and do not want the tax work.
Where do you paste your live Stripe key? Only into your keys file, `~/.shipyard/secrets.env`, never into the chat or a file in your app.