Hermes Wiki
TechResearch/Stripe_Based_Payments/stripe_based_payments

Stripe — setup notes, reusable across projects

Written while setting up payments for the Courses project (courses.fullstackfusions.com) — the first project to actually integrate Stripe. Lives here, alongside clerk_based_auth, as the cross-project reference for the next project that needs payments, the same way Clerk's setup notes live outside any one project. Sections marked Courses-specific don't carry over; everything else does.

1. Is a Stripe account reusable across projects? No — same pattern as Clerk

Create a separate Stripe account per distinct business/product, not one shared account for everything. One Stripe login (your email) can own and switch between multiple separate Stripe accounts — each account has its own API keys (test + live), own products/prices, own webhook endpoints, own payout schedule, own dashboard. This is the same reasoning as "one Clerk app per project": mixing unrelated projects under one account mixes their payout reporting, statement descriptors, and blast radius (an issue on one project's account — disputes, a bad actor, a compliance flag — doesn't spill into another project's account if they're separate).

Exception: if a new "project" is really just another product line under the same business entity/brand (e.g. FullStackFusions sells a second digital product under the same brand), one account with multiple Products can make sense. A genuinely different project/brand should get its own account.

What's reusable is the setup knowledge below, not the account or keys themselves. For a new project: create a fresh Stripe account, make the same category of decisions (§2), get fresh keys (§4), repeat the local testing setup (§5) — but the actual sk_test_.../pk_test_... values and Products are per-project and never shared.

2. Setup decisions — the reusable decision framework

Stripe's onboarding asks the same handful of questions for every new account. Decide each deliberately per project:

Question Options How to decide
How do you want to get started? "Accept payments" vs "Build a platform or marketplace" "Accept payments" unless your app itself routes money to multiple separate third-party sellers (Shopify/Uber/Airbnb/Etsy model — that's Stripe Connect, a much bigger integration). One business selling its own thing = Accept payments, always.
How do you want to charge customers? Non-recurring / Recurring / Invoicing / In-person Non-recurring for pay-once-keep-access products. Recurring only if the product is a subscription with a real renewal/cancellation lifecycle you're ready to model. Invoicing = manual B2B billing, not a self-serve checkout. In-person = physical card readers, irrelevant to a web app.
Who should handle global sales tax/VAT? "Stripe does it" (Managed Payments, +3.5%/transaction) vs "I'll do it" "Stripe does it" for a solo/small operator — global tax compliance (registering and remitting in every country you get a sale from) is a real legal rabbit hole, disproportionate to handle yourself at small scale. The extra 3.5% buys you out of that liability entirely. Reconsider only once volume/margins make the fee meaningful relative to the compliance burden of self-managing.
Setup method "Set up in the Dashboard" vs "Set up with an AI tool" "AI tool" adds Stripe integration guidance directly into Claude/Cursor/Codex (a plugin + a docs-backed skill) — worth it whenever an AI coding assistant is building the integration, which is the default assumption for these notes.

Courses-specific: what was picked

Accept payments, Non-recurring (plan to add Recurring once user base matures), Managed Payments, set up via the AI-tool path (/plugin install stripe@claude-plugins-official + npx skills add https://docs.stripe.com).

3. Pricing — Stripe Standard, Canada

2.9% + CA$0.30 per successful transaction, domestic card, no monthly/setup fee — pure pay-as-you-go. Add-ons: +0.8% international card, +2% currency conversion, +0.5% manually-keyed card. Managed Payments (§2) adds +3.5% on top if selected. No cheaper standard tier exists — the only lower rate is volume-gated custom "IC+" pricing, negotiated directly with Stripe sales once real transaction history exists. Pricing is region-specific — re-check the /pricing page for the country the account is registered in, not the country of individual customers, if setting this up somewhere other than Canada.

4. Environment variables — naming convention and where each belongs

Key Where it lives Why
Publishable key (pk_test_... / pk_live_...) Frontend, prefixed for your framework's public-env convention (VITE_STRIPE_PUBLISHABLE_KEY for Vite, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY for Next.js) Designed to be public — safe to ship in client-side JS.
Secret key (sk_test_... / sk_live_...) Backend only, plain name, no public-prefix (STRIPE_SECRET_KEY) Used server-side to create Checkout Sessions / call Stripe's API. Must never reach the browser bundle — don't prefix it with your framework's public-env marker even by accident, that's the one mistake that actually leaks it.
Webhook signing secret (whsec_...) Backend only (STRIPE_WEBHOOK_SECRET) Verifies an incoming webhook request genuinely came from Stripe, not a forged request hitting your public endpoint. Different value for local CLI testing vs. the deployed production webhook — see §5.

Same "public key in frontend, secret in backend, never swap them" split applies to every project, not just Stripe (same shape as Clerk's publishable/secret key pair).

5. Local testing process (reusable)

  1. Install the Stripe CLI: brew install stripe/stripe-cli/stripe (macOS/Homebrew).
  2. Authenticate: stripe login — opens a browser, links the CLI to your Stripe account.
  3. Forward webhooks to your local backend:
    stripe listen --forward-to localhost:8000/webhooks/stripe
    
    This prints a webhook signing secret valid only for this forwarding session — put it in your backend env as STRIPE_WEBHOOK_SECRET while testing locally. It's different from the signing secret you'll get from the dashboard once you register the real production webhook endpoint URL — don't reuse the CLI one in production, and don't reuse a production one locally.
  4. Trigger a test event two ways:
    • Real flow: go through Checkout with a test card4242 4242 4242 4242, any future expiry (e.g. 12/34), any 3-digit CVC, any postal code. Use 4000 0000 0000 0002 to test a declined payment path instead of a successful one.
    • Synthetic: stripe trigger checkout.session.completed — fires a fake webhook event without an actual checkout, useful for testing just the webhook handler in isolation.
  5. Confirm delivery: Stripe Dashboard (test mode) → Developers → Events (or Webhooks) shows the event and whether your endpoint responded 200.

6. Integration architecture (reusable shape, regardless of project)

  1. Backend creates a Checkout Session server-side (using the secret key), tagged with whatever identifiers your app needs to know who/what was purchased (e.g. user_id, course_id as Stripe metadata) — returns the Checkout URL to the frontend.
  2. Frontend redirects the buyer to that Stripe-hosted Checkout URL. Stripe collects payment, handles 3D Secure/tax/fraud checks itself.
  3. Stripe sends a webhook (checkout.session.completed) to your backend once payment succeeds. Your backend verifies the signature (using STRIPE_WEBHOOK_SECRET), reads back the metadata from step 1, and grants entitlement in your own database.
  4. Your app's actual access-control logic never talks to Stripe directly at request time — it just checks "does this user have an active entitlement record," same as any other grant path (admin-granted, promo code, etc.). Stripe is just one more writer of that record.

Courses-specific: this record is the enrollments table (source='stripe', external_ref=<session/payment id>) — see COURSES_MVP_PLAN.md §8 for the exact schema tie-in. For a new project, the equivalent is whatever table already represents "this user has access to this thing."

7. What to redo per new project (not reusable)

  • A fresh Stripe account and its keys (§1).
  • The decision matrix in §2, made fresh — don't assume the same answers apply.
  • Products/Prices — specific to what's actually being sold.
  • The webhook endpoint URL registered in the Stripe dashboard (differs per project's domain).
  • The local STRIPE_WEBHOOK_SECRET from a fresh stripe listen run (§5) — regenerated every time you start the CLI forwarding, not a stored value.

  • clerk_based_auth — same "one account/app per project, shared setup knowledge" reasoning (§1), same public/secret key split (§4)
  • clerk_production_setup_walkthrough — the Clerk equivalent of this doc: plan vs. actual-dashboard-walkthrough split, worth mirroring here once a second project's Stripe setup surfaces real gotchas
  • TechResearch/_index
Hermes Wiki