<?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: Gumbo Sveins</title>
    <description>The latest articles on DEV Community by Gumbo Sveins (@gumbosveins).</description>
    <link>https://dev.to/gumbosveins</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%2F4123134%2F85bfb54a-cdfb-49e0-a40e-3b337cf92d7e.jpg</url>
      <title>DEV Community: Gumbo Sveins</title>
      <link>https://dev.to/gumbosveins</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gumbosveins"/>
    <language>en</language>
    <item>
      <title>Polar sandbox vs live webhooks — why checkout works and entitlements do not</title>
      <dc:creator>Gumbo Sveins</dc:creator>
      <pubDate>Sun, 13 Sep 2026 13:57:41 +0000</pubDate>
      <link>https://dev.to/gumbosveins/polar-sandbox-vs-live-webhooks-why-checkout-works-and-entitlements-do-not-5gl2</link>
      <guid>https://dev.to/gumbosveins/polar-sandbox-vs-live-webhooks-why-checkout-works-and-entitlements-do-not-5gl2</guid>
      <description>&lt;h1&gt;
  
  
  Polar sandbox vs live webhooks — why checkout works and entitlements do not
&lt;/h1&gt;

&lt;p&gt;Checkout can succeed and your app can still treat the buyer as a free user. On Polar that usually means the &lt;strong&gt;webhook&lt;/strong&gt; landed in the wrong world — sandbox secret on a live deploy, production endpoint still pointed at last night’s preview URL, or a signature check that never ran.&lt;/p&gt;

&lt;p&gt;Article one covered the whole Next.js / Vercel go-live pass. This one is only Polar: &lt;strong&gt;sandbox vs production&lt;/strong&gt;, and the webhook path that turns a paid order into an entitlement in &lt;em&gt;your&lt;/em&gt; database.&lt;/p&gt;

&lt;p&gt;Official Polar docs win when they disagree with anything here. Links below were fetched live from polar.sh/docs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Polar does not have a Stripe-style “test mode”
&lt;/h2&gt;

&lt;p&gt;Polar’s sandbox is a &lt;strong&gt;separate environment&lt;/strong&gt;, not a toggle on your live org. From &lt;a href="https://polar.sh/docs/integrate/sandbox" rel="noopener noreferrer"&gt;Sandbox Environment&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's a dedicated server, completely isolated from the production instance…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why? Polar is a Merchant of Record. They keep live money movements clean by isolating test data so it never interferes. You get unlimited sandbox accounts and orgs for experiments.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sandbox&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://sandbox.polar.sh" rel="noopener noreferrer"&gt;sandbox.polar.sh&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polar.sh" rel="noopener noreferrer"&gt;polar.sh&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://sandbox-api.polar.sh/v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://api.polar.sh/v1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;Minted in sandbox only&lt;/td&gt;
&lt;td&gt;Minted in live org only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Products / customers / webhooks&lt;/td&gt;
&lt;td&gt;Isolated&lt;/td&gt;
&lt;td&gt;Isolated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Card for happy-path tests&lt;/td&gt;
&lt;td&gt;Stripe test &lt;code&gt;4242 4242 4242 4242&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Real cards (or your live discounts)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://polar.sh/docs/api-reference/2026-04/introduction" rel="noopener noreferrer"&gt;API Overview&lt;/a&gt; is blunt: production tokens cannot talk to the sandbox API, and the reverse is also true. Create &lt;strong&gt;separate tokens in each environment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sandbox rate limits are lower (100 req/min vs 500 in production). That rarely bites a checkout flow; it does bite scripts that hammer the sandbox while you debug.&lt;/p&gt;

&lt;p&gt;One sandbox limitation that surprises people (&lt;a href="https://polar.sh/docs/integrate/sandbox" rel="noopener noreferrer"&gt;same sandbox doc&lt;/a&gt;): customer-facing emails in sandbox only go to &lt;strong&gt;members of your organization&lt;/strong&gt;. Sub-addressing like &lt;code&gt;you+test@example.com&lt;/code&gt; is accepted. Do not expect a random Gmail to get order confirmation mail from sandbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four secrets that must travel together
&lt;/h2&gt;

&lt;p&gt;Treat these as a matched set. Mixing any one row across worlds is how you get “checkout works, entitlement missing.”&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret / ID&lt;/th&gt;
&lt;th&gt;Preview &amp;amp; laptop&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POLAR_ACCESS_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Token from sandbox.polar.sh&lt;/td&gt;
&lt;td&gt;Token from the live org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;POLAR_SERVER&lt;/code&gt; / SDK &lt;code&gt;server&lt;/code&gt; or &lt;code&gt;environment&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sandbox&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;production&lt;/code&gt; (or omit — SDKs default to production)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POLAR_WEBHOOK_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Secret from the &lt;strong&gt;sandbox&lt;/strong&gt; webhook endpoint&lt;/td&gt;
&lt;td&gt;Secret from the &lt;strong&gt;live&lt;/strong&gt; webhook endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POLAR_PRODUCT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sandbox product UUID&lt;/td&gt;
&lt;td&gt;Live product UUID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official Next.js guide tells you to start on sandbox and later flip &lt;code&gt;server: "sandbox"&lt;/code&gt; to &lt;code&gt;"production"&lt;/code&gt; (&lt;a href="https://polar.sh/docs/guides/nextjs" rel="noopener noreferrer"&gt;Integrate Polar with Next.js&lt;/a&gt;). The Next adapter’s &lt;code&gt;Checkout&lt;/code&gt;, &lt;code&gt;CustomerPortal&lt;/code&gt;, and &lt;code&gt;Webhooks&lt;/code&gt; helpers take the same idea (&lt;a href="https://polar.sh/docs/integrate/sdk/adapters/nextjs" rel="noopener noreferrer"&gt;Next.js adapter&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Newer SDK previews use &lt;code&gt;environment: "sandbox"&lt;/code&gt; on &lt;code&gt;createPolar&lt;/code&gt; (&lt;a href="https://polar.sh/docs/api-reference/2026-04/introduction" rel="noopener noreferrer"&gt;API Overview&lt;/a&gt;). Older &lt;code&gt;@polar-sh/nextjs&lt;/code&gt; examples use &lt;code&gt;server: "sandbox"&lt;/code&gt;. Either way: &lt;strong&gt;token origin must match the flag&lt;/strong&gt;. A sandbox token with &lt;code&gt;server: "production"&lt;/code&gt; fails in confusing 401s, not a clean error message.&lt;/p&gt;

&lt;p&gt;Never prefix any of these with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;. Organization access tokens are server-only; Polar will revoke leaked OATs via secret scanning (&lt;a href="https://polar.sh/docs/api-reference/2026-04/introduction" rel="noopener noreferrer"&gt;API Overview&lt;/a&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Webhooks are the source of truth for entitlements
&lt;/h2&gt;

&lt;p&gt;A success redirect proves the browser finished checkout. It does &lt;strong&gt;not&lt;/strong&gt; prove your database granted access. Polar’s own Next.js guide frames webhooks as how you keep your DB in sync with checkouts, orders, and subscriptions (&lt;a href="https://polar.sh/docs/guides/nextjs" rel="noopener noreferrer"&gt;guide&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Minimum production shape with the official adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/webhook/polar/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Webhooks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@polar-sh/nextjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Webhooks&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;webhookSecret&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;POLAR_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onOrderPaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Idempotent upsert: grant entitlement keyed by order.id&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onCustomerStateChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Mirror Polar's view of benefits / subscriptions&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;Prefer granular handlers (&lt;code&gt;onOrderPaid&lt;/code&gt;, &lt;code&gt;onBenefitGrantCreated&lt;/code&gt;, …) over a giant &lt;code&gt;onPayload&lt;/code&gt; switch once you know which events you subscribed to. The adapter documents the full list (&lt;a href="https://polar.sh/docs/integrate/sdk/adapters/nextjs" rel="noopener noreferrer"&gt;Next.js adapter&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Register two endpoints, not one
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://polar.sh/docs/integrate/webhooks/endpoints" rel="noopener noreferrer"&gt;Setup Webhooks&lt;/a&gt; walks the dashboard flow: Add Endpoint → absolute URL → Raw delivery → secret → event subscription.&lt;/p&gt;

&lt;p&gt;Do that &lt;strong&gt;twice&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox org&lt;/strong&gt; → tunnel or a long-lived staging URL while you develop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live org&lt;/strong&gt; → &lt;code&gt;https://your.domain/api/webhook/polar&lt;/code&gt; (stable custom domain).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Polar cannot reach &lt;code&gt;localhost&lt;/code&gt;. For laptop work use either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;polar listen http://localhost:3000/&lt;/code&gt; (Polar CLI tunnel — prints a forwarding secret; see &lt;a href="https://polar.sh/docs/integrate/webhooks/endpoints" rel="noopener noreferrer"&gt;Setup Webhooks&lt;/a&gt;), or&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ngrok http 3000&lt;/code&gt; / Cloudflare Tunnel and paste the https URL into the &lt;strong&gt;sandbox&lt;/strong&gt; webhook (&lt;a href="https://polar.sh/docs/guides/nextjs" rel="noopener noreferrer"&gt;Next.js guide&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Never&lt;/strong&gt; register the production webhook against a Vercel preview URL. Preview hostnames change every commit. Deliveries will 404 by Thursday while Polar still shows “checkout succeeded.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Signature verification is non-negotiable
&lt;/h3&gt;

&lt;p&gt;Polar signs payloads so you can reject forgeries. &lt;a href="https://polar.sh/docs/integrate/webhooks/delivery" rel="noopener noreferrer"&gt;Handle &amp;amp; monitor webhook deliveries&lt;/a&gt; shows &lt;code&gt;validateEvent&lt;/code&gt; from &lt;code&gt;@polar-sh/sdk/webhooks&lt;/code&gt; (and the Python equivalent). The &lt;code&gt;@polar-sh/nextjs&lt;/code&gt; &lt;code&gt;Webhooks()&lt;/code&gt; helper does this for you when you pass &lt;code&gt;webhookSecret&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Important timing note from Polar (as of docs fetched 13 Sep 2026):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Secrets generated on or after 8 September 2026, 00:00 UTC follow Standard Webhooks. Older secrets use Polar HMAC.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you roll your own verification, read that page carefully — older secrets need different key handling than new &lt;code&gt;whsec_…&lt;/code&gt; Standard Webhooks secrets. SDK versions &lt;code&gt;1.0.0-alpha.19+&lt;/code&gt; try both. Prefer the official helper over hand-rolled HMAC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Delivery rules that break entitlements in production
&lt;/h3&gt;

&lt;p&gt;From the same &lt;a href="https://polar.sh/docs/integrate/webhooks/delivery" rel="noopener noreferrer"&gt;delivery doc&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;What it means for you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timeout &lt;strong&gt;10s&lt;/strong&gt; (aim for &lt;strong&gt;&amp;lt;2s&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;Ack fast; queue heavy work (grant email, CRM sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry up to &lt;strong&gt;10×&lt;/strong&gt; with backoff&lt;/td&gt;
&lt;td&gt;Handlers &lt;strong&gt;must be idempotent&lt;/strong&gt; — replay must not double-grant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;10 consecutive failures&lt;/strong&gt; → endpoint disabled&lt;/td&gt;
&lt;td&gt;Org members get email; you must re-enable manually after fixing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No redirect following&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;www&lt;/code&gt; ↔ apex 301s count as failure. Point Polar at the final URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare &lt;strong&gt;Bot Fight Mode&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Can 403 Polar even if you allowlist IPs — disable BFM or fix WAF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth middleware&lt;/td&gt;
&lt;td&gt;Exclude &lt;code&gt;/api/webhook/polar&lt;/code&gt; so Polar is not asked for a session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also allowlist Polar’s egress IPs if your firewall requires it (list on the delivery page — Polar called out a new production IP &lt;code&gt;3.134.178.243&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete failure timeline (and how to read the dashboard)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Monday:&lt;/strong&gt; PR preview works. You register a sandbox webhook on &lt;code&gt;https://my-app-git-feat-xyz.vercel.app/api/webhook/polar&lt;/code&gt;. Sandbox checkout → webhook → row in DB. Green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wednesday:&lt;/strong&gt; You merge to &lt;code&gt;main&lt;/code&gt;, set live &lt;code&gt;POLAR_ACCESS_TOKEN&lt;/code&gt;, forget to create a &lt;strong&gt;live&lt;/strong&gt; webhook (or you copy the preview URL into the live org). Customer pays on the custom domain. Polar shows a paid order. Your production DB never hears about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thursday:&lt;/strong&gt; Preview deployment from Monday is gone. Even the sandbox endpoint is 404ing. You look at Vercel Runtime Logs and see nothing. Polar’s webhook delivery page shows the red rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First move every time:&lt;/strong&gt; Polar org (the one that took the money) → Settings → Webhooks → delivery history. You want recent &lt;strong&gt;2xx&lt;/strong&gt;. Then Vercel Runtime Logs for &lt;code&gt;POST /api/webhook/polar&lt;/code&gt;. If Polar has 2xx and your DB is empty, the handler is wrong or not idempotent. If Polar has 404/403/3xx, fix the URL / middleware / Cloudflare before touching business logic.&lt;/p&gt;

&lt;p&gt;Redeliver from the dashboard after you fix the route — Polar supports manual redelivery (&lt;a href="https://polar.sh/docs/integrate/webhooks/delivery" rel="noopener noreferrer"&gt;delivery doc&lt;/a&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Go-live sequence for Polar only
&lt;/h2&gt;

&lt;p&gt;Do this as its own checklist the evening you flip live payments — separate from “does the homepage render.”&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm you are logged into the &lt;strong&gt;live&lt;/strong&gt; org on polar.sh (not sandbox.polar.sh).&lt;/li&gt;
&lt;li&gt;Recreate products in live if you only built them in sandbox. Copy &lt;strong&gt;live&lt;/strong&gt; product IDs into Production-scoped env — do not paste sandbox UUIDs.&lt;/li&gt;
&lt;li&gt;Mint a &lt;strong&gt;live&lt;/strong&gt; organization access token. Scope it Production-only in Vercel.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;POLAR_SERVER=production&lt;/code&gt; (or remove &lt;code&gt;server: "sandbox"&lt;/code&gt; / &lt;code&gt;environment: "sandbox"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Add webhook &lt;code&gt;https://your.domain/api/webhook/polar&lt;/code&gt; on the live org. Paste the new signing secret into Production &lt;code&gt;POLAR_WEBHOOK_SECRET&lt;/code&gt;. Subscribe at least to order-paid / benefit-grant events you actually handle.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;POLAR_SUCCESS_URL&lt;/code&gt; to the production origin with &lt;code&gt;{CHECKOUT_ID}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redeploy so server env (and any bake-time public config) matches.&lt;/li&gt;
&lt;li&gt;Run one live test: cheap product, 100% discount, or a payment you are willing to refund. Confirm: Polar delivery &lt;strong&gt;2xx&lt;/strong&gt;, Runtime Log line, entitlement row, customer portal sees the benefit.&lt;/li&gt;
&lt;li&gt;Watch deliveries for 30 minutes. Keep Instant Rollback one click away.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Until that 2xx exists, do not post the buy link as “live.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Soft sell (kit, not a live guarantee)
&lt;/h2&gt;

&lt;p&gt;I packed the Polar scope cheat sheet, the full ~60-point launch checklist, five &lt;code&gt;.env.example&lt;/code&gt; packs (including &lt;code&gt;02-polar.env.example&lt;/code&gt;), and a preview→prod runbook into a &lt;strong&gt;$19&lt;/strong&gt; download:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://buy.polar.sh/polar_cl_uxzixqNM81nkPoIW3um9aacYzAmA1bXQbWqUe3EQE75" rel="noopener noreferrer"&gt;https://buy.polar.sh/polar_cl_uxzixqNM81nkPoIW3um9aacYzAmA1bXQbWqUe3EQE75&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checkout may still be enabling payments on Polar’s side — treat the link as the kit storefront once KYC / payouts are fully live, not as a promise that the card form works this second. Everything above still works as free reading tonight.&lt;/p&gt;

&lt;p&gt;Educational material only. Re-read Polar’s &lt;a href="https://polar.sh/docs/integrate/sandbox" rel="noopener noreferrer"&gt;sandbox&lt;/a&gt;, &lt;a href="https://polar.sh/docs/integrate/webhooks/endpoints" rel="noopener noreferrer"&gt;webhook setup&lt;/a&gt;, &lt;a href="https://polar.sh/docs/integrate/webhooks/delivery" rel="noopener noreferrer"&gt;delivery&lt;/a&gt;, and &lt;a href="https://polar.sh/docs/guides/nextjs" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; guides before you take real money. Vendor names belong to their owners.&lt;/p&gt;

&lt;p&gt;Ship Polar when sandbox checkout → webhook → entitlement already passed on a non-production URL, live products and secrets are Production-scoped, and the &lt;strong&gt;live&lt;/strong&gt; webhook delivery page shows a recent 2xx for an order you caused on purpose.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>polar</category>
      <category>webdev</category>
      <category>payments</category>
    </item>
    <item>
      <title>The Next.js / Vercel production env mistakes that break launches</title>
      <dc:creator>Gumbo Sveins</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:30:32 +0000</pubDate>
      <link>https://dev.to/gumbosveins/the-nextjs-vercel-production-env-mistakes-that-break-launches-1d8g</link>
      <guid>https://dev.to/gumbosveins/the-nextjs-vercel-production-env-mistakes-that-break-launches-1d8g</guid>
      <description>&lt;h1&gt;
  
  
  The Next.js / Vercel production env mistakes that break launches
&lt;/h1&gt;

&lt;p&gt;You can ship a Next.js app that works on your laptop and still wake up to a broken production URL. The usual cause is not a bad feature — it is env scope, a sandbox token in the live org, or a webhook still pointed at last night’s preview hostname.&lt;/p&gt;

&lt;p&gt;This is a go-live pass for solo engineers on the common 2026 stack: &lt;strong&gt;Next.js App Router, Vercel, Polar, Neon, Resend, Supabase&lt;/strong&gt;. It is a human checklist, not an automated scan. Checking a box does not make the app secure. Official vendor docs win when they disagree with anything here.&lt;/p&gt;

&lt;p&gt;I packed the full ~60-point version, five &lt;code&gt;.env.example&lt;/code&gt; packs, and a preview→prod runbook into a &lt;strong&gt;$19 kit&lt;/strong&gt;: &lt;a href="https://buy.polar.sh/polar_cl_uxzixqNM81nkPoIW3um9aacYzAmA1bXQbWqUe3EQE75" rel="noopener noreferrer"&gt;https://buy.polar.sh/polar_cl_uxzixqNM81nkPoIW3um9aacYzAmA1bXQbWqUe3EQE75&lt;/a&gt;. Below is enough of the real material to run a serious launch review tonight, even if you never buy it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug under most “it worked in preview” failures
&lt;/h2&gt;

&lt;p&gt;Vercel gives you three worlds: &lt;strong&gt;Development&lt;/strong&gt;, &lt;strong&gt;Preview&lt;/strong&gt;, and &lt;strong&gt;Production&lt;/strong&gt;. Mixing them is the most common launch bug.&lt;/p&gt;

&lt;p&gt;Before you touch secrets, answer out loud: which Polar org, which database, and which Resend &lt;code&gt;from&lt;/code&gt; address does each world use? If you cannot fill a cell, stop.&lt;/p&gt;

&lt;p&gt;Also confirm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The production branch in Vercel is the branch you think it is (&lt;code&gt;main&lt;/code&gt; unless you changed it).&lt;/li&gt;
&lt;li&gt;Preview deployments are on, and a PR URL is &lt;strong&gt;not&lt;/strong&gt; the production hostname.&lt;/li&gt;
&lt;li&gt;You will not register the &lt;strong&gt;production&lt;/strong&gt; Polar webhook against a changing preview URL.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Ten checks that catch real production outages
&lt;/h2&gt;

&lt;p&gt;These are lifted from the full checklist. Work them top to bottom on a copy you own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets and Vercel scopes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;4.&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.production&lt;/code&gt;, and &lt;code&gt;.env*.local&lt;/code&gt; are in &lt;code&gt;.gitignore&lt;/code&gt;. &lt;code&gt;git ls-files | grep env&lt;/code&gt; should return nothing useful to an attacker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5.&lt;/strong&gt; No secret is prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;. That prefix inlines the value into the browser bundle. Tokens, service-role keys, webhook secrets, and database URLs never get it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.&lt;/strong&gt; Every variable in the Vercel dashboard has an explicit scope: Production, Preview, Development. “Apply to all” is only for non-secret public config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7.&lt;/strong&gt; Production Polar tokens, production database URLs, and the live Resend key are scoped &lt;strong&gt;Production only&lt;/strong&gt;. Preview gets its own sandbox / branch values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9.&lt;/strong&gt; &lt;code&gt;AUTH_SECRET&lt;/code&gt; / &lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt; is at least 32 bytes (&lt;code&gt;openssl rand -base64 32&lt;/code&gt;) and &lt;strong&gt;different&lt;/strong&gt; in preview and production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10.&lt;/strong&gt; &lt;code&gt;NEXT_PUBLIC_APP_URL&lt;/code&gt; (and &lt;code&gt;AUTH_URL&lt;/code&gt; / &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; if you use Auth.js) equals the origin of that environment. No leftover &lt;code&gt;*.vercel.app&lt;/code&gt; in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11.&lt;/strong&gt; &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; is baked at &lt;code&gt;next build&lt;/code&gt;. Changing the dashboard value does nothing until you redeploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth (one that people skip)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;16.&lt;/strong&gt; Every Server Action and Route Handler that mutates data re-checks the session &lt;strong&gt;inside&lt;/strong&gt; the function. A layout or proxy check is not enough.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;World&lt;/th&gt;
&lt;th&gt;Git&lt;/th&gt;
&lt;th&gt;Polar&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;laptop&lt;/td&gt;
&lt;td&gt;sandbox&lt;/td&gt;
&lt;td&gt;local or a personal Neon/Supabase branch&lt;/td&gt;
&lt;td&gt;Resend test addresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview&lt;/td&gt;
&lt;td&gt;every PR&lt;/td&gt;
&lt;td&gt;sandbox&lt;/td&gt;
&lt;td&gt;disposable Neon branch or staging Supabase&lt;/td&gt;
&lt;td&gt;Resend test addresses — never real customers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;live org + live products&lt;/td&gt;
&lt;td&gt;production Neon / Supabase&lt;/td&gt;
&lt;td&gt;verified domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Billing, database, email (the three that usually explode on day one)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;22.&lt;/strong&gt; Polar: &lt;code&gt;POLAR_SERVER&lt;/code&gt; / &lt;code&gt;server:&lt;/code&gt; in the SDK matches the token. A sandbox token with &lt;code&gt;server: "production"&lt;/code&gt; fails in confusing ways. Sandbox and production are different servers, different tokens, different products, different webhook secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;25.&lt;/strong&gt; The production webhook endpoint is an absolute public URL Polar can reach — not localhost, not a PR preview URL. Signature is verified with &lt;code&gt;POLAR_WEBHOOK_SECRET&lt;/code&gt;. Preview URLs change every commit; deliveries will 404 by Thursday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;29.&lt;/strong&gt; Neon: the app uses the &lt;strong&gt;pooled&lt;/strong&gt; connection string (&lt;code&gt;-pooler.&lt;/code&gt; host). Migrations use the &lt;strong&gt;direct&lt;/strong&gt; / unpooled string. Using the direct URL as the runtime URL on Vercel Functions is how you get &lt;code&gt;too many connections&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;36.&lt;/strong&gt; Resend: production &lt;code&gt;from&lt;/code&gt; uses a domain you verified at resend.com/domains. &lt;code&gt;onboarding@resend.dev&lt;/code&gt; is test-only and must not ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;40.&lt;/strong&gt; Preview and local send only to Resend test addresses (&lt;code&gt;delivered@resend.dev&lt;/code&gt;, &lt;code&gt;bounced@resend.dev&lt;/code&gt;) or your own inbox. Preview must not email real users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase (if you use it)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;44.&lt;/strong&gt; RLS is enabled on every table in &lt;code&gt;public&lt;/code&gt; that the Data API can see. A table with RLS off is public to anyone holding the anon / publishable key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three env pitfalls from the actual packs
&lt;/h2&gt;

&lt;p&gt;These comments live in the kit’s &lt;code&gt;.env.example&lt;/code&gt; files because they keep biting people.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;NEXT_PUBLIC_APP_URL&lt;/code&gt; without a redeploy
&lt;/h3&gt;

&lt;p&gt;From the Next/Vercel pack: the value is inlined at build time. After you change it in Vercel you &lt;strong&gt;must&lt;/strong&gt; redeploy. Editing the variable alone does not patch an already-built bundle. Same rule for any other &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;Symptom from the runbook: build OK, runtime &lt;code&gt;undefined&lt;/code&gt; env — or auth looping on the custom domain because &lt;code&gt;AUTH_URL&lt;/code&gt; is still &lt;code&gt;*.vercel.app&lt;/code&gt;. Fix: set the production origin, redeploy, clear cookies.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Polar sandbox token in production (or the reverse)
&lt;/h3&gt;

&lt;p&gt;From the Polar pack: mint the token in the &lt;strong&gt;same&lt;/strong&gt; environment you will call. Preview / Development → token from &lt;a href="https://sandbox.polar.sh" rel="noopener noreferrer"&gt;sandbox.polar.sh&lt;/a&gt;. Production  token from the live organization. &lt;code&gt;POLAR_SERVER&lt;/code&gt; must be &lt;code&gt;sandbox&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt; to match.&lt;/p&gt;

&lt;p&gt;Also: sandbox product IDs are invalid in production. Copy IDs from the dashboard of the matching org. Production webhook must be your custom domain, not a PR preview URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Neon pooled vs direct on Vercel
&lt;/h3&gt;

&lt;p&gt;From the Neon pack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_URL&lt;/code&gt; — pooled (&lt;code&gt;-pooler.&lt;/code&gt; host) → Server Components, Route Handlers, Server Actions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_URL_UNPOOLED&lt;/code&gt; — direct → migrations only (&lt;code&gt;prisma migrate deploy&lt;/code&gt;, &lt;code&gt;drizzle-kit migrate&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never use the unpooled string as the runtime URL on Vercel Functions. Always keep &lt;code&gt;sslmode=require&lt;/code&gt;. Confirm that Production still points at the main branch, not a leftover preview branch from the Vercel + Neon integration.&lt;/p&gt;

&lt;p&gt;Bonus Resend trap, same class of mistake: shipping with &lt;code&gt;EMAIL_FROM=…&amp;lt;onboarding@resend.dev&amp;gt;&lt;/code&gt;. Development and Preview can keep that; Production needs &lt;code&gt;Name &amp;lt;you@verified-domain&amp;gt;&lt;/code&gt; and a new deploy after you change it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Preview → production in one sitting
&lt;/h2&gt;

&lt;p&gt;Do not interleave “I’ll fix env later” with a &lt;code&gt;git push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;laptop  --push PR--&amp;gt;  Preview URL  --merge main--&amp;gt;  Production URL
   |                      |                              |
 sandbox Polar        sandbox Polar                 live Polar
 Neon branch          Neon branch                   Neon main
 Resend test          Resend test                   verified domain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Preview first.&lt;/strong&gt; Fill Preview-scoped secrets (sandbox Polar, Neon branch, Resend test recipients, staging Supabase). Pull env locally, run &lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;, open a PR, smoke-test auth / a write / a sandbox checkout / a test email on the preview URL. Merge only when that is green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production values before the merge.&lt;/strong&gt; Production scope only for live Polar token + webhook secret, &lt;code&gt;POLAR_SERVER=production&lt;/code&gt;, pooled + unpooled Neon main URLs, live Resend key + verified &lt;code&gt;EMAIL_FROM&lt;/code&gt;, production Supabase project keys, and a &lt;strong&gt;new&lt;/strong&gt; &lt;code&gt;AUTH_SECRET&lt;/code&gt;. Mark tokens Sensitive. Do not tick Preview on those rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attach domain and webhook.&lt;/strong&gt; Certificate valid → Polar live webhook on &lt;code&gt;https://your.domain/api/webhook/polar&lt;/code&gt;  Resend domain verified with one real message to yourself → run migrations against &lt;code&gt;DATABASE_URL_UNPOOLED&lt;/code&gt; → then ship &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediately after deploy:&lt;/strong&gt; private window, home / sign-up / sign-in / sign-out, one write path, one Polar live test (or 100% discount) with webhook row + entitlement confirmed, one email from the verified domain, custom domain + HTTPS + www/apex, and a 30-minute log watch before you post the URL. Keep the previous production deploy one click away for Instant Rollback. If you migrated the database forward, do not promote an old deploy until you have a down migration or a restore point.&lt;/p&gt;




&lt;h2&gt;
  
  
  When something is already on fire
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;First move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build OK, runtime &lt;code&gt;undefined&lt;/code&gt; env&lt;/td&gt;
&lt;td&gt;Scoped to Preview only, or &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; changed without redeploy&lt;/td&gt;
&lt;td&gt;Check scope → redeploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polar 401 / empty webhook&lt;/td&gt;
&lt;td&gt;Sandbox token in production, or &lt;code&gt;server&lt;/code&gt; mismatch&lt;/td&gt;
&lt;td&gt;Compare token origin to &lt;code&gt;POLAR_SERVER&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>vercel</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
