The Playbook unlocked traqqit.com →

Module 5 · ~15 min

💳 Add accounts + Stripe billing

The one place laziness stops. You'll leave able to charge for an app without ever leaking a secret key or trusting the browser.

When to add money

Only when the app earns it. Most apps ship free first; you add billing when there's a real reason someone would pay. Billing is the one genuinely un-lazy part of the stack — so only pay that cost once it's justified. A free app that people use beats a paywalled app nobody found.

Two shapes of "get paid"

Pick the simplest that fits. One-time has no portal, no dunning, no renewal logic — reach for it whenever the product allows.

The un-lazy boundary — memorize these four

  1. The secret key stays server-side. The browser only ever receives a hosted Stripe redirect URL. The secret key touches nothing client-side, ever.
  2. Entitlements are server-verified. Whether someone paid is decided by your server checking Stripe — never a flag the client can set. Never "fake-unlock."
  3. Webhook signatures are verified. Anyone can POST to your webhook URL. Only trust an event whose Stripe signature validates over the raw body, within a replay window.
  4. Checkout is hosted. Card details go to Stripe's page, not yours. You build almost no billing UI and never touch a card number.

The one-time gate, end to end (what this course uses)

Three server routes. No database, no user table, no email required. The elegant part: the proof of purchase IS the Stripe session, re-verified server-side each visit — so the buyer's link keeps working forever, on any device, because the server just re-asks Stripe.

// 1) create a one-time Checkout session — the secret key lives HERE, server-side
const s = await stripe('POST', '/v1/checkout/sessions', {
  mode: 'payment',
  'line_items[0][price]': PRICE_ID,
  'line_items[0][quantity]': 1,
  success_url: BASE + '/members?session_id={CHECKOUT_SESSION_ID}',
  cancel_url:  BASE + '/?checkout=cancel',
});
return { url: s.url };          // the browser only ever sees this hosted URL

// 2) the gate — is this session really paid, and for OUR product?
async function paid(sessionId){
  const s = await stripe('GET',
    '/v1/checkout/sessions/' + sessionId + '?expand[]=line_items');
  return s.payment_status === 'paid'
      && s.mode === 'payment'
      && s.line_items.data.some(li => li.price.id === PRICE_ID);
}
// paid() true -> set a signed cookie + serve the content. false -> bounce to checkout.

// 3) webhook — verify the signature over the RAW body before trusting anything
//    header "t=<ts>,v1=<sig>";  expected = HMAC_SHA256(secret, t + '.' + rawBody)
//    constant-time compare, and reject if |now - t| > 300s (replay window)

⚠️ That "for OUR product?" line-item check is load-bearing if your Stripe account sells more than one thing. A paid session for a different product must never unlock this one. Don't drop it.

The subscription shape (when you outgrow one-time)

For recurring access you add two things: somewhere to remember who a user is, and a way to keep "what they've paid for" in sync with Stripe.

Never fake-unlock

The mistake that bites beginners: gating in the browser — hiding paid content with CSS or JavaScript. Anyone can view-source. If content is worth money, the server must serve it only after verifying payment, and the protected files must live outside any public static folder. That's exactly how these lessons reach you: they're served by a tiny gated server, not sitting in a public directory.

🦥 Lazy where you can, strict where it counts. Everything else in this course was "smallest thing that works." Money is the exception — here you do it properly, because the failure mode is someone's card or your content leaking.

Your turn ✅