<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anthony Loren</title>
    <description>The latest articles on DEV Community by Anthony Loren (@anthony_loren_8bbba86df65).</description>
    <link>https://dev.to/anthony_loren_8bbba86df65</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4232993%2F4275b6b7-0a25-4ed3-8d51-9ac980ae84ab.png</url>
      <title>DEV Community: Anthony Loren</title>
      <link>https://dev.to/anthony_loren_8bbba86df65</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anthony_loren_8bbba86df65"/>
    <language>en</language>
    <item>
      <title>Stripe Checkout works. Why is my Next.js app still showing unpaid?</title>
      <dc:creator>Anthony Loren</dc:creator>
      <pubDate>Sun, 08 Nov 2026 01:54:45 +0000</pubDate>
      <link>https://dev.to/anthony_loren_8bbba86df65/stripe-checkout-works-why-is-my-nextjs-app-still-showing-unpaid-1ajf</link>
      <guid>https://dev.to/anthony_loren_8bbba86df65/stripe-checkout-works-why-is-my-nextjs-app-still-showing-unpaid-1ajf</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://helpfulmods.com/blog/nextjs-stripe-checkout-paid-access/" rel="noopener noreferrer"&gt;Helpful Mods&lt;/a&gt;. I build Helpful Mods, which sells the Funnel service used in this walkthrough.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc496t9a601wtdiljkyzf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc496t9a601wtdiljkyzf.webp" alt="Two Helpful Mods robots reaching toward one another" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The customer finishes Checkout, lands on your success page, and clicks the thing they just paid for. Your app sends them straight back to pricing.&lt;/p&gt;

&lt;p&gt;Before changing the redirect, follow that purchase into the application. Which user owns it? Has the payment worker processed it? What does the paid page read when the customer arrives?&lt;/p&gt;

&lt;p&gt;The interesting part is often between those steps. Stripe can have a completed payment while your app is still waiting for a worker, looking up another customer, or serving an old access result.&lt;/p&gt;

&lt;p&gt;Here's how I'd connect a Next.js App Router app to &lt;a href="https://helpfulmods.com/mods/funnel/" rel="noopener noreferrer"&gt;Funnel and Revenue&lt;/a&gt; and work through it. Funnel runs beside the app as a Node.js service with PostgreSQL and a background worker. Your existing login and application database stay involved throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one purchase and its IDs
&lt;/h2&gt;

&lt;p&gt;Open the failing purchase and put these values next to each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;What it connects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App user ID&lt;/td&gt;
&lt;td&gt;The signed-in person opening the paid page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funnel customer ID&lt;/td&gt;
&lt;td&gt;That person's saved customer journey.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout ID&lt;/td&gt;
&lt;td&gt;The purchase attempt queued in Funnel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe Session ID&lt;/td&gt;
&lt;td&gt;The Checkout Session created for that attempt.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This catches a surprisingly ordinary problem: the customer pays in one account, then opens your app in a browser signed into another. It also catches code that creates the right purchase but reads access for a different customer.&lt;/p&gt;

&lt;p&gt;Save the Funnel customer ID against your app user in the database. Later routes can find it from the current session. If your app already has a billing-account record, that's a sensible home for the mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect the Next.js backend to Funnel
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/antanarr/helpfulmods-nextjs-paid-access" rel="noopener noreferrer"&gt;example on GitHub&lt;/a&gt; includes &lt;code&gt;lib/funnel-client.mjs&lt;/code&gt;, a small adapter for the calls below. Put it behind a server-only wrapper in your app and supply the &lt;code&gt;signToken&lt;/code&gt; helper from your purchased Funnel package's &lt;code&gt;dist/core/auth.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;funnel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFunnelClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FUNNEL_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FUNNEL_TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;signToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HM_AUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FUNNEL_ORIGIN&lt;/code&gt; is the origin of your installed service. &lt;code&gt;HM_AUTH_SECRET&lt;/code&gt; is the same private signing secret configured there. The wrapper lives in your backend, where it can use that secret and read your existing authentication session.&lt;/p&gt;

&lt;p&gt;Shape the verified session result as &lt;code&gt;{userId, email, emailVerified}&lt;/code&gt;, then capture the customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;funnel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actualConsentChoice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter calls &lt;code&gt;POST /api/funnel/commands/capture&lt;/code&gt; with the email and consent choice. It signs the app user ID as the token's &lt;code&gt;sub&lt;/code&gt;; Funnel uses that actor as the customer owner. Save the returned &lt;code&gt;customerId&lt;/code&gt; before moving on to checkout.&lt;/p&gt;

&lt;p&gt;If you get &lt;code&gt;verified_email_required&lt;/code&gt;, inspect the token claims. Funnel needs &lt;code&gt;verifiedEmail&lt;/code&gt; from the account your authentication provider has verified. A user can have a valid app session while their email is still unverified, or your adapter can accidentally leave the verified claim out. This is worth checking before spending an afternoon on Stripe settings.&lt;/p&gt;

&lt;p&gt;The example adapter creates short-lived, customer-scoped tokens for each call. Your Route Handler obtains the current session and customer mapping first. The &lt;a href="https://nextjs.org/docs/app/guides/authentication" rel="noopener noreferrer"&gt;Next.js authentication guide&lt;/a&gt; has useful examples of doing that authorization close to the data request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the checkout worker time to return a URL
&lt;/h2&gt;

&lt;p&gt;The checkout command is &lt;code&gt;POST /api/funnel/commands/checkout&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"THE_SAVED_CUSTOMER_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"THE_SAVED_PURCHASE_INTENT_KEY"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the adapter, that becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;funnel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;savedRequestKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save one request key for the attempt. When the response gets lost or someone double-clicks, retrying that key continues the same work. Creating a new key on every click gives you more attempts to untangle.&lt;/p&gt;

&lt;p&gt;The command returns a &lt;code&gt;checkoutId&lt;/code&gt; and a &lt;code&gt;jobId&lt;/code&gt;. The worker then creates the Stripe Session and saves its URL. Read the &lt;code&gt;customer&lt;/code&gt; command with &lt;code&gt;{customerId}&lt;/code&gt; and find the entry matching your saved checkout ID. The adapter does that lookup for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;funnel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkoutStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkoutId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Return this URL to the checkout interface so it can navigate there.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preparing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put a “Preparing checkout…” state here and poll your authenticated status route with a bounded delay. If the URL never appears, inspect the saved job and worker heartbeat. An idle worker can make the checkout button look broken even though the request was saved successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the payment back into the app
&lt;/h2&gt;

&lt;p&gt;Your Stripe Snapshot webhook destination for this installation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://YOUR_FUNNEL_SERVICE/webhooks/stripe/funnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure that destination's signing secret as &lt;code&gt;HM_STRIPE_WEBHOOK_SECRET&lt;/code&gt;. Funnel verifies the delivery and queues a read of the current Stripe resource. The same worker that prepared Checkout also needs to be running for this part.&lt;/p&gt;

&lt;p&gt;When payment succeeded but access hasn't changed, Stripe's delivery history is a useful next stop. Look at the destination URL and response. An old development endpoint or a signing secret from another destination can leave a perfectly good payment disconnected from the deployed application.&lt;/p&gt;

&lt;p&gt;Then follow the saved job into the checkout record. A received event tells you the request arrived; the reconciliation result tells you what the app learned about the purchase.&lt;/p&gt;

&lt;p&gt;If the delivery history shows the same purchase arriving more than once, &lt;a href="https://helpfulmods.com/blog/stripe-webhook-duplicate-events/" rel="noopener noreferrer"&gt;our duplicate-webhook guide&lt;/a&gt; follows the event and purchase IDs into a database transaction. That's useful when one payment creates two fulfillment jobs, or a replay appears to change an already-settled purchase.&lt;/p&gt;

&lt;p&gt;Stripe's &lt;a href="https://docs.stripe.com/checkout/fulfillment" rel="noopener noreferrer"&gt;Checkout fulfillment guide&lt;/a&gt; explains another reason to build this path: customers sometimes pay and close the tab before reaching your success page. Their purchase still needs to reach the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read current access where the paid resource is served
&lt;/h2&gt;

&lt;p&gt;Funnel's &lt;code&gt;access&lt;/code&gt; command takes &lt;code&gt;{customerId}&lt;/code&gt; and returns &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;offerIds&lt;/code&gt;. The example adapter checks for active access to its configured offer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;funnel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaymentStatus&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PaidWorkspace&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put that check in the backend operation that serves the paid data as well as the page. If reconciliation is still running, show the current payment status and a way to check again. That gives the customer something more useful than another trip to pricing.&lt;/p&gt;

&lt;p&gt;For the paid page, use dynamic rendering and a fresh service read. The adapter sets &lt;code&gt;cache: "no-store"&lt;/code&gt;; your own access endpoint should also send &lt;code&gt;Cache-Control: no-store&lt;/code&gt;. Otherwise, a cached unpaid result can outlive the payment that should have replaced it. The &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/route" rel="noopener noreferrer"&gt;Next.js Route Handler reference&lt;/a&gt; covers the Request and Response APIs used by the adapter.&lt;/p&gt;

&lt;p&gt;A fresh access check also picks up later changes. A refund, expired subscription or billing hold should affect the next protected request, including when the customer has kept the same tab open.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://helpfulmods.com/blog/stripe-checkout-customer-access/" rel="noopener noreferrer"&gt;Stripe took the payment. Now give the customer access.&lt;/a&gt; goes further into those rules, including failed renewals and partial refunds. Once the Next.js connection works, those are good cases to put through the same access screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the awkward states while you're building
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://helpfulmods.com/examples/nextjs-paid-access.zip" rel="noopener noreferrer"&gt;Next.js example ZIP&lt;/a&gt; gives you a local app where you can start a sample checkout, leave access pending, simulate confirmation and apply a later hold. Open the protected resource after each change and watch what the server returns. The sample states make the waiting and recovery screens easy to work on before connecting your own Stripe account.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;npm test&lt;/code&gt; and &lt;code&gt;npm run dev&lt;/code&gt;, then open &lt;code&gt;http://127.0.0.1:3000&lt;/code&gt;. The &lt;a href="https://github.com/antanarr/helpfulmods-nextjs-paid-access" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; has the adapter, tests and instructions for connecting it to your existing auth and customer records.&lt;/p&gt;

&lt;p&gt;Once those pieces are connected, take the original failing purchase through the same path again. You should be able to point to the app user, their saved checkout, the worker's reconciliation result and the current access response. That's a much better explanation of “paid” than the page the browser happened to land on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://helpfulmods.com/mods/funnel/" rel="noopener noreferrer"&gt;Funnel and Revenue&lt;/a&gt; supplies the customer journey, durable checkout jobs, Stripe reconciliation and access reads used here.&lt;/p&gt;

&lt;p&gt;Written with AI assistance. The implementation and example repository are linked throughout.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>stripe</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
