<?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: MrLong</title>
    <description>The latest articles on DEV Community by MrLong (@myjoinlong).</description>
    <link>https://dev.to/myjoinlong</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%2F4216443%2F57bfafb0-e8c8-49e0-bb55-51e2d1f4ac02.png</url>
      <title>DEV Community: MrLong</title>
      <link>https://dev.to/myjoinlong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/myjoinlong"/>
    <language>en</language>
    <item>
      <title>Vercel /api works in production but returns 404 in Vite? Here's my local fix</title>
      <dc:creator>MrLong</dc:creator>
      <pubDate>Fri, 30 Oct 2026 08:59:18 +0000</pubDate>
      <link>https://dev.to/myjoinlong/vercel-api-works-in-production-but-returns-404-in-vite-heres-my-local-fix-3mmb</link>
      <guid>https://dev.to/myjoinlong/vercel-api-works-in-production-but-returns-404-in-vite-heres-my-local-fix-3mmb</guid>
      <description>&lt;p&gt;I was testing a Vue 3 + Supabase + Stripe flow locally. The frontend loaded and signup worked, but clicking Upgrade returned a 404 for /api/create-checkout-session.&lt;/p&gt;

&lt;p&gt;The endpoint worked on Vercel. The mismatch was the local runtime: vite dev serves the frontend and HMR, but it does not execute files in Vercel's /api directory.&lt;/p&gt;

&lt;p&gt;There are two reasonable fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run vercel dev and use Vercel's local runtime.&lt;/li&gt;
&lt;li&gt;Run a small Node server for the API handlers, then proxy /api from Vite to that server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose the second option so local testing uses the same handler files as deployment. The request path is:&lt;/p&gt;

&lt;p&gt;Browser → Vite :5173 → /api proxy → local Node shim :5175 → same handler&lt;/p&gt;

&lt;p&gt;The Vite proxy is small:&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&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;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:5175&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;changeOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;The shim routes known paths to the actual handler modules. It adapts Node request/response objects to the shape those handlers expect, rather than maintaining a second copy of checkout or webhook logic.&lt;/p&gt;

&lt;p&gt;Two details took longer to catch than the proxy itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load .env before importing handlers. The handlers read Stripe and Supabase variables at module load time, so loading .env afterward is too late.&lt;/li&gt;
&lt;li&gt;Preserve the webhook's raw request body. Stripe signature verification uses the original bytes; parsing and re-serializing the body can invalidate an otherwise correct signature.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I run the frontend and API shim separately:&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 dev
npm run dev:api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I test checkout with Stripe test mode and confirm the webhook updates the Supabase subscription row. The live demo is also a sandbox and does not charge a real card.&lt;/p&gt;

&lt;p&gt;I wrote up the implementation and tradeoffs in this &lt;a href="https://saasplate.dev/guides/vercel-api-local-dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=saas_starter_local_api" rel="noopener noreferrer"&gt;step-by-step guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://huanglongjian.gumroad.com/l/saas-starter?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=saas_starter_local_api" rel="noopener noreferrer"&gt;Vue 3 + Supabase + Stripe source kit&lt;/a&gt; includes the local API shim and notes. It is a separate one-time $29 purchase; the live demo is only a test-mode sandbox.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: AI assistance was used to prepare this wording; the project and implementation are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>stripe</category>
    </item>
    <item>
      <title>Supabase RLS for SaaS Billing: Keep Stripe Subscription Status Server-Side</title>
      <dc:creator>MrLong</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:30:38 +0000</pubDate>
      <link>https://dev.to/myjoinlong/supabase-rls-for-saas-billing-keep-stripe-subscription-status-server-side-1n54</link>
      <guid>https://dev.to/myjoinlong/supabase-rls-for-saas-billing-keep-stripe-subscription-status-server-side-1n54</guid>
      <description>&lt;p&gt;When adding subscriptions to a Supabase app, the browser should never decide that a user is paid. Keep billing state on the server and use Row Level Security (RLS) to limit what each signed-in user can read.&lt;/p&gt;

&lt;p&gt;A common source of confusion is assuming that an “update your own profile” policy makes every field on that row safe to edit. RLS policies control which rows a query can affect; PostgreSQL grants control which actions a role can perform. Billing fields such as subscription_status should be written only by trusted server-side code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The risky pattern
&lt;/h2&gt;

&lt;p&gt;Imagine a profiles table with an owner-scoped update policy and a broad table grant:&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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users update own profile"&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;profiles&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&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;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;delete&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;profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That policy answers “which row?” It does not make subscription_status an approved column. A signed-in user who can update their own row could try to change that field from the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep billing writes server-side
&lt;/h2&gt;

&lt;p&gt;If the browser only needs to read a user’s profile, a narrower permission setup is easier to reason about. Keep an owner-scoped SELECT policy in place, remove direct table writes from normal client roles, and let trusted server code process billing updates:&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;revoke&lt;/span&gt; &lt;span class="k"&gt;all&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;profiles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;select&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;profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;grant&lt;/span&gt; &lt;span class="k"&gt;all&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;profiles&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;service_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This SQL changes table privileges; it does not create the SELECT policy. Keep a policy such as auth.uid() = id for profile reads. If users need editable fields, grant only those specific columns and keep subscription fields outside that allowlist. Adapt the grants to your schema and any signup flow that inserts profiles.&lt;/p&gt;

&lt;p&gt;The Supabase service role key must stay on the server and must never be included in a frontend bundle. In a Stripe integration, update billing state after verifying the webhook signature against the untouched request body. A checkout success redirect is not proof of payment and should not unlock the paid UI by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the demo
&lt;/h2&gt;

&lt;p&gt;I built a small Vue 3 + Supabase + Stripe SaaS starter to practice this end-to-end flow. The hosted demo uses Stripe test mode; its sample $19/month price is a test price. The source kit is a separate, one-time Gumroad purchase.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://saasplate.dev" rel="noopener noreferrer"&gt;Open the sandbox demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huanglongjian.gumroad.com/l/saas-starter?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=saas_starter_launch" rel="noopener noreferrer"&gt;Get the source kit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used AI assistance to edit the wording. The project and implementation are mine.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>sql</category>
      <category>saas</category>
    </item>
    <item>
      <title>Designing Spatial Synergies for a 4x4 Auto-Battler</title>
      <dc:creator>MrLong</dc:creator>
      <pubDate>Fri, 30 Oct 2026 05:44:30 +0000</pubDate>
      <link>https://dev.to/myjoinlong/designing-spatial-synergies-for-a-4x4-auto-battler-477g</link>
      <guid>https://dev.to/myjoinlong/designing-spatial-synergies-for-a-4x4-auto-battler-477g</guid>
      <description>&lt;p&gt;I built a small browser auto-battler prototype in about ten days to answer one question: can arranging items in a backpack make the next fight more interesting?&lt;/p&gt;

&lt;p&gt;Pack Tactics uses a 4×4 inventory grid. Between waves, you drag items into the backpack, rotate them to make them fit, and merge copies up to level 5. Where an item sits can also matter because neighboring items can activate synergies. Then the next wave puts that layout to the test.&lt;/p&gt;

&lt;p&gt;The prototype is written in TypeScript and rendered with Canvas 2D. I kept the scope small so I could test the interaction loop before adding more content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop I wanted to test
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the items available between waves.&lt;/li&gt;
&lt;li&gt;Decide what to keep, combine, rotate, or move.&lt;/li&gt;
&lt;li&gt;Arrange the grid around space and adjacency.&lt;/li&gt;
&lt;li&gt;Start the next fight and see what the setup does.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part is that the backpack should change the player's decision, not just store rewards. A strong item can be useful by itself, while moving it next to another item may create a different payoff. Merging copies gives another choice: improve one item, or keep more pieces available for a different arrangement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design questions I used to evaluate it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does placement matter after the shop?
&lt;/h3&gt;

&lt;p&gt;If moving an item only makes the backpack look tidier, the grid is busywork. Adjacency effects give placement a reason to matter in the next fight. Rotation adds a spatial constraint, so fitting an item and keeping a useful neighbor relationship can pull in different directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can players understand why a layout worked?
&lt;/h3&gt;

&lt;p&gt;The next wave is the feedback for the previous arrangement. If players cannot connect an outcome to the items they placed, the synergy rules need clearer previews or combat feedback. That is one of the first things I would watch in a playtest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is merging a meaningful trade-off?
&lt;/h3&gt;

&lt;p&gt;Combining copies can make an item stronger, but it also changes how many pieces occupy the grid and which adjacency options remain. I want that to feel like a build decision rather than an automatic upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would test next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Can a new player predict which neighboring items will interact before starting a wave?&lt;/li&gt;
&lt;li&gt;Does rotating an item create an interesting choice, or only extra clicks?&lt;/li&gt;
&lt;li&gt;After a fight, can the player name one layout decision that helped or hurt?&lt;/li&gt;
&lt;li&gt;Does the 4×4 space feel constrained in a useful way across several waves?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I made the prototype in the browser so the interaction can be tried without installing anything. The current interface is in Chinese:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Play the prototype:&lt;/strong&gt; &lt;a href="https://floral-queen-7f68.huanglongjian7515.workers.dev/" rel="noopener noreferrer"&gt;https://floral-queen-7f68.huanglongjian7515.workers.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build strategy games, I would value feedback on one thing: what is the clearest way you have found to show adjacency rules before a player commits to a layout?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared with AI writing assistance. The project details and design questions are based on my own prototype.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
