<?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: ShipTested</title>
    <description>The latest articles on DEV Community by ShipTested (@shiptested).</description>
    <link>https://dev.to/shiptested</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%2F4034950%2F6ae88d39-66f5-46b3-8e05-9a46f5a0c8b4.png</url>
      <title>DEV Community: ShipTested</title>
      <link>https://dev.to/shiptested</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shiptested"/>
    <language>en</language>
    <item>
      <title>A Success URL Is Not Proof of Payment</title>
      <dc:creator>ShipTested</dc:creator>
      <pubDate>Sat, 18 Jul 2026 09:47:33 +0000</pubDate>
      <link>https://dev.to/shiptested/a-success-url-is-not-proof-of-payment-3il8</link>
      <guid>https://dev.to/shiptested/a-success-url-is-not-proof-of-payment-3il8</guid>
      <description>&lt;p&gt;A customer finishes checkout and the payment provider sends their browser back&lt;br&gt;
to &lt;code&gt;/success&lt;/code&gt;. Your app shows a green check. That is useful user experience. It&lt;br&gt;
is not evidence that the order was paid.&lt;/p&gt;

&lt;p&gt;The browser controls the URL it visits. It can revisit that URL, change its&lt;br&gt;
query string, share it, or open it without completing checkout. If visiting&lt;br&gt;
&lt;code&gt;/success?paid=true&lt;/code&gt; creates access, the paywall is a URL convention—not an&lt;br&gt;
authorization boundary.&lt;/p&gt;

&lt;p&gt;The launch check is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A success-page visit must create zero entitlements.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Reproduce the failure
&lt;/h2&gt;

&lt;p&gt;The ShipTested public teaching harness contains this deliberately unsafe&lt;br&gt;
handler:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;vulnerableHandleSuccessPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entitlements&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&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="nx"&gt;entitlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;browser-redirect&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="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;Nothing here establishes that a payment provider accepted an order. Both&lt;br&gt;
&lt;code&gt;paid&lt;/code&gt; and &lt;code&gt;user_id&lt;/code&gt; came from the browser. A visitor can type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/success?paid=true&amp;amp;user_id=user-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exploit test calls the handler with that query and verifies that it creates&lt;br&gt;
an entitlement. Run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One naming detail matters: the exploit test passes when it successfully&lt;br&gt;
reproduces the vulnerable behavior. It is evidence of a failure, not launch&lt;br&gt;
approval.&lt;/p&gt;

&lt;p&gt;This example is intentionally small. It does not claim that every checkout&lt;br&gt;
integration has this bug. It gives you a deterministic way to recognize and&lt;br&gt;
test one unsafe pattern in your own app.&lt;/p&gt;
&lt;h2&gt;
  
  
  State the invariant before choosing the fix
&lt;/h2&gt;

&lt;p&gt;The fixed example makes the success page informational only:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSuccessPage&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;{&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;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending-verified-webhook&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its launch test supplies the same hostile query and checks two outcomes:&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending-verified-webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entitlements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model still returns status &lt;code&gt;200&lt;/code&gt; and &lt;code&gt;pending-verified-webhook&lt;/code&gt;. It&lt;br&gt;
demonstrates an informational response without creating access, regardless of&lt;br&gt;
the supplied query parameters.&lt;/p&gt;

&lt;p&gt;Run the green checks with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The checkout check passes only when a success-page visit creates no&lt;br&gt;
entitlement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should create access instead?
&lt;/h2&gt;

&lt;p&gt;Put the authority on the server side. A common payment flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An authenticated server endpoint maps an internal plan name to an
allowlisted provider product or variant.&lt;/li&gt;
&lt;li&gt;The server attaches the signed-in internal user ID as checkout metadata. The
browser does not choose the owner, price, or variant as trusted data.&lt;/li&gt;
&lt;li&gt;The success page shows a pending or processing state. It may read current
entitlement state, but it does not write it.&lt;/li&gt;
&lt;li&gt;A server-side verified payment event creates the entitlement. The planned
full ShipTested reference flow uses a signed webhook whose event type, paid
status, store, variant, test mode, order ID, and internal user ID are checked
before a state change.&lt;/li&gt;
&lt;li&gt;Duplicate deliveries become no-ops through a database-enforced boundary
such as a transaction and appropriate unique constraints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A signed webhook is not the only possible provider architecture. Some&lt;br&gt;
integrations retrieve and verify a checkout session server-to-server after the&lt;br&gt;
redirect. The invariant stays the same: browser-controlled redirect data does&lt;br&gt;
not grant access by itself.&lt;/p&gt;

&lt;p&gt;The public checkout example proves only the negative redirect invariant. It&lt;br&gt;
does not yet contain the server-created checkout endpoint. That endpoint,&lt;br&gt;
durable database constraints, refund handling, and provider-specific&lt;br&gt;
integration belong to the full reference repository.&lt;/p&gt;

&lt;p&gt;The public webhook model checks only that the custom user ID is a non-empty&lt;br&gt;
string. A production flow must resolve it to an eligible internal record before&lt;br&gt;
granting access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the check against your staging app
&lt;/h2&gt;

&lt;p&gt;Use a staging environment and test data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the checkout success URL.&lt;/li&gt;
&lt;li&gt;Open it in a fresh session without placing an order.&lt;/li&gt;
&lt;li&gt;Refresh it several times.&lt;/li&gt;
&lt;li&gt;Add or change parameters such as &lt;code&gt;paid&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;variant&lt;/code&gt;, and
&lt;code&gt;user_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspect the entitlement table or access-control result after every attempt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The passing result is zero new access. A message saying “payment processing” is&lt;br&gt;
fine. A new entitlement, paid role, license, credit balance, or protected&lt;br&gt;
download is a failure.&lt;/p&gt;

&lt;p&gt;Then complete a sandbox purchase and verify the separate server-owned&lt;br&gt;
fulfillment path. That second step matters, but it is outside what the small&lt;br&gt;
redirect harness proves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary of this proof
&lt;/h2&gt;

&lt;p&gt;The repository is a dependency-free Node.js teaching harness with in-memory&lt;br&gt;
state. It demonstrates an unsafe redirect and a fixed invariant through&lt;br&gt;
executable tests. It is not a production payment integration, a security audit,&lt;br&gt;
or a certification. Passing this one test does not make an app ready to sell.&lt;/p&gt;

&lt;p&gt;It does remove one dangerous ambiguity: the browser may report where it landed.&lt;br&gt;
It does not decide who paid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/shiptested/public-example" rel="noopener noreferrer"&gt;Inspect the exact test&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shiptested.github.io/checklist/" rel="noopener noreferrer"&gt;Run the broader launch-readiness checklist&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://docs.lemonsqueezy.com/guides/developer-guide/taking-payments" rel="noopener noreferrer"&gt;Lemon Squeezy — Taking Payments&lt;/a&gt;,&lt;br&gt;
&lt;a href="https://docs.lemonsqueezy.com/api/checkouts/create-checkout" rel="noopener noreferrer"&gt;Create a Checkout API&lt;/a&gt;,&lt;br&gt;
and &lt;a href="https://docs.lemonsqueezy.com/help/checkout/passing-custom-data" rel="noopener noreferrer"&gt;Passing Custom Data&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;ShipTested uses AI heavily to produce this material and verifies the technical&lt;br&gt;
examples by running them. There are no fake testimonials and no guaranteed&lt;br&gt;
outcomes — the point is that you run the tests yourself instead of trusting an&lt;br&gt;
author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>security</category>
      <category>payments</category>
    </item>
    <item>
      <title>Test Your Supabase RLS With Two Users Before You Sell</title>
      <dc:creator>ShipTested</dc:creator>
      <pubDate>Sat, 18 Jul 2026 09:20:04 +0000</pubDate>
      <link>https://dev.to/shiptested/test-your-supabase-rls-with-two-users-before-you-sell-815</link>
      <guid>https://dev.to/shiptested/test-your-supabase-rls-with-two-users-before-you-sell-815</guid>
      <description>&lt;p&gt;Your prototype works. That is not the same as safe to sell.&lt;/p&gt;

&lt;p&gt;The dashboard opens. User A sees User A’s documents. User B sees User B’s&lt;br&gt;
documents. That looks like data isolation, but it may only prove that the page&lt;br&gt;
requested or displayed a filtered result.&lt;/p&gt;

&lt;p&gt;If the database still accepts a direct request for another account’s row, the&lt;br&gt;
boundary has already failed. User B does not have to use the screen you&lt;br&gt;
designed. They can call the same data API and request User A’s row by ID.&lt;/p&gt;

&lt;p&gt;The useful launch question is concrete:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can User B read User A’s row?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Start with a red test
&lt;/h2&gt;

&lt;p&gt;The ShipTested public example makes this failure visible with a small Node.js&lt;br&gt;
20 teaching harness. It has no package dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run demo
npm run &lt;span class="nb"&gt;test&lt;/span&gt;:failures
npm run &lt;span class="nb"&gt;test&lt;/span&gt;:launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deliberately vulnerable function returns every document it receives. It&lt;br&gt;
ignores the requesting user:&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exploit test asks for documents as User B and checks whether a row owned by&lt;br&gt;
User A is present.&lt;/p&gt;

&lt;p&gt;When that test passes, it means the vulnerable behavior was successfully&lt;br&gt;
reproduced. It is a red exploit demonstration, not launch approval.&lt;/p&gt;

&lt;p&gt;The fixed policy model makes the intended invariant explicit:&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requestingUserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its launch check expects User B to receive only &lt;code&gt;doc-b&lt;/code&gt;, never &lt;code&gt;doc-a&lt;/code&gt;. Run&lt;br&gt;
&lt;code&gt;npm run test:launch&lt;/code&gt; to exercise the fixed invariants. Read both versions side&lt;br&gt;
by side; the diff is the point.&lt;/p&gt;
&lt;h2&gt;
  
  
  Move the invariant to the database
&lt;/h2&gt;

&lt;p&gt;A JavaScript filter can explain the rule, but it cannot enforce a Supabase&lt;br&gt;
database boundary. In a real project, Row Level Security must be enabled on the&lt;br&gt;
relevant table, and the applied policies must match the operations your&lt;br&gt;
application permits.&lt;/p&gt;

&lt;p&gt;The example includes this minimal SELECT policy shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users can read their own documents"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This says that an authenticated request may select a document only when the&lt;br&gt;
authenticated user’s ID matches &lt;code&gt;user_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is intentionally narrow. It does not define INSERT, UPDATE, or DELETE&lt;br&gt;
policies, and it does not account for your project’s grants, schema, ownership&lt;br&gt;
rules, functions, or other tables.&lt;/p&gt;

&lt;p&gt;That boundary matters: the repository’s JavaScript check is a policy model. The&lt;br&gt;
SQL is an example policy shape. Neither proves that your live migrations are&lt;br&gt;
applied correctly or that your Supabase project is ready to launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the two-user test against your project
&lt;/h2&gt;

&lt;p&gt;Use two ordinary test accounts and the same client path your application uses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in as User A and create a private row.&lt;/li&gt;
&lt;li&gt;Save that row’s ID.&lt;/li&gt;
&lt;li&gt;Sign out, then sign in as User B.&lt;/li&gt;
&lt;li&gt;Through the normal data API, request User A’s exact row ID.&lt;/li&gt;
&lt;li&gt;Repeat the isolation check for every operation your app permits: insert,
update, and delete as well as select.&lt;/li&gt;
&lt;li&gt;Sign back in as User A and confirm the intended own-row flow still works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not use a service-role key for this test. It bypasses RLS and cannot show&lt;br&gt;
what an ordinary authenticated user is allowed to do.&lt;/p&gt;

&lt;p&gt;Do not run the test with production data. Synthetic users and rows are enough.&lt;/p&gt;

&lt;p&gt;For the read case, the passing invariant is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User B receives zero rows owned by User A, while User A can still retrieve&lt;br&gt;
the row.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both halves matter. If neither user can read anything because the query or&lt;br&gt;
migration is broken, you have not demonstrated correct isolation.&lt;/p&gt;

&lt;p&gt;For mutations, assert the state after each request. User B must not be able to&lt;br&gt;
create a row attributed to User A, change User A’s row, or delete it. User A&lt;br&gt;
should still be able to complete each operation the product intentionally&lt;br&gt;
supports.&lt;/p&gt;

&lt;p&gt;If User B receives or changes User A’s data, keep the exact request, response,&lt;br&gt;
identities, and minimal fixture. Turn that reproduction into a test before&lt;br&gt;
changing the policy. A stable red case gives you evidence that the fix&lt;br&gt;
addresses the failure you actually found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know what green does—and does not—mean
&lt;/h2&gt;

&lt;p&gt;A passing two-user check is evidence for one tested invariant on one applied&lt;br&gt;
database path.&lt;/p&gt;

&lt;p&gt;It is not a security certification.&lt;/p&gt;

&lt;p&gt;It does not automatically cover every table, storage bucket, RPC, view, server&lt;br&gt;
route, or privileged function. It also does not prove that future migrations&lt;br&gt;
preserve the policy.&lt;/p&gt;

&lt;p&gt;Keep the test in the launch suite and run it when the schema or authorization&lt;br&gt;
path changes. Add cases as the product adds operations.&lt;/p&gt;

&lt;p&gt;The goal is not to claim that an app is “secure.” The goal is to preserve a&lt;br&gt;
falsifiable statement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User B cannot read or mutate User A’s rows through the ordinary authenticated&lt;br&gt;
path, and User A can still complete the intended flow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your prototype working is the start. A red/green isolation test is evidence you&lt;br&gt;
can use before you sell it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/shiptested/public-example" rel="noopener noreferrer"&gt;Inspect the exact test&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shiptested.github.io/checklist/" rel="noopener noreferrer"&gt;Run the broader launch-readiness checklist&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase Row Level Security&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://supabase.com/docs/guides/database/testing" rel="noopener noreferrer"&gt;database testing&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;ShipTested uses AI heavily to produce this material and verifies the technical&lt;br&gt;
examples by running them. There are no fake testimonials and no guaranteed&lt;br&gt;
outcomes — the point is that you run the tests yourself instead of trusting an&lt;br&gt;
author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One Webhook, Five Deliveries, One Entitlement</title>
      <dc:creator>ShipTested</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:30:03 +0000</pubDate>
      <link>https://dev.to/shiptested/one-webhook-five-deliveries-one-entitlement-3ol9</link>
      <guid>https://dev.to/shiptested/one-webhook-five-deliveries-one-entitlement-3ol9</guid>
      <description>&lt;p&gt;A customer can complete checkout once while your webhook handler receives the&lt;br&gt;
same event more than once. If every delivery creates access again, one purchase&lt;br&gt;
can produce duplicate licenses, seats, credits, or subscription records.&lt;/p&gt;

&lt;p&gt;The launch check is concrete:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Take one valid, signed order webhook. Deliver the exact same body five times.&lt;br&gt;
The result should still be one processed-event record and one intended&lt;br&gt;
entitlement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A webhook signature is necessary. It is not idempotency.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the signature proves—and what it does not
&lt;/h2&gt;

&lt;p&gt;A valid signature tells your handler that the raw request body matches a&lt;br&gt;
message signed with the configured webhook secret. It helps reject forged or&lt;br&gt;
modified payloads before they change state.&lt;/p&gt;

&lt;p&gt;It does not tell you that the message is new.&lt;/p&gt;

&lt;p&gt;Every repeated delivery can carry a valid signature because it is an authentic&lt;br&gt;
copy of the same event. A handler that verifies the signature and then blindly&lt;br&gt;
calls &lt;code&gt;grantAccess()&lt;/code&gt; can still grant access five times.&lt;/p&gt;

&lt;p&gt;In the deliberately vulnerable ShipTested example, the handler parses the body&lt;br&gt;
and appends an entitlement on every call. Replaying the fixture five times&lt;br&gt;
leaves five entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same order webhook ×5
entitlements.length === 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the failure we preserve as a reproducible red test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the invariant before the implementation
&lt;/h2&gt;

&lt;p&gt;The fixed example starts with an invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For one accepted &lt;code&gt;order_created&lt;/code&gt; event and one expected product variant,&lt;br&gt;
repeated sequential delivery changes entitlement state once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The handler verifies the HMAC signature against the unmodified raw body before&lt;br&gt;
mutation. It then checks the fields this teaching fixture expects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event name is &lt;code&gt;order_created&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;resource type is &lt;code&gt;orders&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;order ID is present;&lt;/li&gt;
&lt;li&gt;payment status is &lt;code&gt;paid&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;store ID matches;&lt;/li&gt;
&lt;li&gt;variant ID matches;&lt;/li&gt;
&lt;li&gt;order and item test mode match the configured environment;&lt;/li&gt;
&lt;li&gt;the custom internal user ID is a non-empty string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A signed payload for an unexpected variant grants nothing. So does an invalid&lt;br&gt;
signature.&lt;/p&gt;

&lt;p&gt;After those checks, the example creates a stable key from the event name,&lt;br&gt;
resource type, and order ID:&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;eventKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&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="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first accepted delivery records that key and sets the intended entitlement.&lt;br&gt;
Later deliveries with the same key return &lt;code&gt;duplicate-no-op&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The five-delivery test
&lt;/h2&gt;

&lt;p&gt;The fixed test signs one raw JSON body once, then sends that same body and&lt;br&gt;
signature through the handler five times sequentially.&lt;/p&gt;

&lt;p&gt;Its expected outcomes are explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. granted-once
2. duplicate-no-op
3. duplicate-no-op
4. duplicate-no-op
5. duplicate-no-op

processedEvents.size === 1
entitlements.size === 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is stronger evidence than clicking through checkout once and looking at a&lt;br&gt;
success page. The test forces the duplicate-delivery path and records the&lt;br&gt;
invariant in code.&lt;/p&gt;

&lt;p&gt;Clone the teaching harness and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run &lt;span class="nb"&gt;test&lt;/span&gt;:launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository contains both the vulnerable reproduction and the fixed&lt;br&gt;
sequential replay test. The fixtures use test-only values and contain no live&lt;br&gt;
credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this result actually proves
&lt;/h2&gt;

&lt;p&gt;The test proves that this dependency-free JavaScript model handles five&lt;br&gt;
sequential copies of the same signed fixture as one intended entitlement&lt;br&gt;
change.&lt;/p&gt;

&lt;p&gt;It also demonstrates that four controls solve different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signature verification rejects a body with an invalid signature;&lt;/li&gt;
&lt;li&gt;payload allowlisting rejects an authentic but unexpected order;&lt;/li&gt;
&lt;li&gt;the event key recognizes a repeated accepted event;&lt;/li&gt;
&lt;li&gt;the entitlement key represents the intended access state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the evidence boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not prove
&lt;/h2&gt;

&lt;p&gt;The public example stores processed events in an in-memory &lt;code&gt;Set&lt;/code&gt; and&lt;br&gt;
entitlements in an in-memory &lt;code&gt;Map&lt;/code&gt;. A process restart clears both.&lt;/p&gt;

&lt;p&gt;Its five deliveries run in a normal loop. They do not arrive concurrently. The&lt;br&gt;
example therefore does not prove that two workers cannot pass a database&lt;br&gt;
existence check at the same time.&lt;/p&gt;

&lt;p&gt;It also does not prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;that the custom user ID exists in your database;&lt;/li&gt;
&lt;li&gt;atomic rollback after a failure between event recording and entitlement
mutation;&lt;/li&gt;
&lt;li&gt;behavior across multiple server instances;&lt;/li&gt;
&lt;li&gt;refund or revocation ordering;&lt;/li&gt;
&lt;li&gt;protection against a late purchase event restoring revoked access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are launch checks for the real database-backed application, not claims&lt;br&gt;
made by this harness.&lt;/p&gt;

&lt;p&gt;In production, the “already processed?” check and the entitlement change need&lt;br&gt;
a database-enforced boundary. Use a transaction and appropriate unique&lt;br&gt;
constraints so two overlapping deliveries cannot both win. Resolve the buyer&lt;br&gt;
against a real internal record. Then test forced failures, concurrent delivery,&lt;br&gt;
process restarts, and refund ordering against the deployed database revision.&lt;/p&gt;

&lt;p&gt;Do not label the in-memory demonstration production-safe. Do not treat one&lt;br&gt;
green sequential replay as a security certification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the smallest useful test
&lt;/h2&gt;

&lt;p&gt;Start with the test that can fail today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;capture one valid test-mode webhook without exposing its secret;&lt;/li&gt;
&lt;li&gt;replay the exact body and signature five times in a safe environment;&lt;/li&gt;
&lt;li&gt;inspect processed-event and entitlement state;&lt;/li&gt;
&lt;li&gt;save the result with the commit and environment tested.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If five deliveries create five grants, keep that reproduction. It is better&lt;br&gt;
evidence than a vague feeling that retries are “probably handled.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/shiptested/public-example" rel="noopener noreferrer"&gt;Inspect the exact test&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://shiptested.github.io/checklist/" rel="noopener noreferrer"&gt;Run the broader launch-readiness checklist&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://docs.lemonsqueezy.com/help/webhooks/webhook-requests" rel="noopener noreferrer"&gt;Lemon Squeezy webhook requests&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://docs.lemonsqueezy.com/help/webhooks/signing-requests" rel="noopener noreferrer"&gt;signing requests&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;ShipTested uses AI heavily to produce this material and verifies the technical&lt;br&gt;
examples by running them. There are no fake testimonials and no guaranteed&lt;br&gt;
outcomes — the point is that you run the tests yourself instead of trusting an&lt;br&gt;
author.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>security</category>
      <category>node</category>
    </item>
  </channel>
</rss>
