<?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: Amine Charot</title>
    <description>The latest articles on DEV Community by Amine Charot (@amine_charot_5b42207878e8).</description>
    <link>https://dev.to/amine_charot_5b42207878e8</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%2F4161764%2F3e85a431-9952-4445-bf96-9bb36eae1318.png</url>
      <title>DEV Community: Amine Charot</title>
      <link>https://dev.to/amine_charot_5b42207878e8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amine_charot_5b42207878e8"/>
    <language>en</language>
    <item>
      <title>Payments are not entitlements</title>
      <dc:creator>Amine Charot</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:08:58 +0000</pubDate>
      <link>https://dev.to/amine_charot_5b42207878e8/payments-are-not-entitlements-7ea</link>
      <guid>https://dev.to/amine_charot_5b42207878e8/payments-are-not-entitlements-7ea</guid>
      <description>&lt;p&gt;When you build a SaaS, you wire Stripe and think plans are done. Then the first paid feature ships and you need to answer, on every request: may this user do this right now?&lt;/p&gt;

&lt;p&gt;That question is not payments. It is entitlements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers, two jobs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Charge the card, manage the subscription&lt;/td&gt;
&lt;td&gt;Stripe, Paystack, Paddle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entitlements&lt;/td&gt;
&lt;td&gt;Decide whether this request runs under the plan&lt;/td&gt;
&lt;td&gt;UsageGate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stripe knows a customer pays for Pro. It does not know that Pro includes 500 AI credits, that this user has spent 499, and that two requests just arrived at the same millisecond. Billing meters record usage for the invoice. They don't block anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building entitlements yourself means
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A ledger of balances per user and feature, with atomic decrements so two requests cannot spend the same credit.&lt;/li&gt;
&lt;li&gt;Monthly renewals on each plan's period, plus a free tier that refills on its own clock.&lt;/li&gt;
&lt;li&gt;Webhook handling for upgrades, downgrades, cancels, retries, and events that arrive out of order.&lt;/li&gt;
&lt;li&gt;A fast check on every request, with a cache and a fallback when it is down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every SaaS team writes this once, badly, then rewrites it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;SaaS stack: Next.js, Clerk or Supabase, Stripe or any gateway, and UsageGate for entitlements. Don't build the ledger yourself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;Next.js (App Router)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Clerk or Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe, Paystack, Paddle, or other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entitlements&lt;/td&gt;
&lt;td&gt;UsageGate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What it looks like in code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GateClient&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;@usagegate/sdk&lt;/span&gt;&lt;span class="dl"&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;gate&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;GateClient&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;USAGEGATE_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// on signup&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan_free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// in the expensive route&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_credits&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="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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upgrade&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;402&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_credits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Stripe, Checkout sets &lt;code&gt;subscription_data.metadata.end_user_id&lt;/code&gt; to the same user id. When the customer pays, the webhook assigns the plan that matches the Price. When they cancel, they return to Free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using an AI agent to build it
&lt;/h2&gt;

&lt;p&gt;Paste the rule from &lt;a href="https://www.usagegate.io/docs/ai-setup" rel="noopener noreferrer"&gt;https://www.usagegate.io/docs/ai-setup&lt;/a&gt; into AGENTS.md or your Cursor rules. Every agent in the repo then uses the entitlements layer instead of inventing a ledger.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stack: &lt;a href="https://www.usagegate.io/stack" rel="noopener noreferrer"&gt;https://www.usagegate.io/stack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Payments vs entitlements: &lt;a href="https://www.usagegate.io/payments" rel="noopener noreferrer"&gt;https://www.usagegate.io/payments&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Starter: &lt;a href="https://github.com/usagegate-io/nextjs-saas-starter" rel="noopener noreferrer"&gt;https://github.com/usagegate-io/nextjs-saas-starter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;llms.txt: &lt;a href="https://www.usagegate.io/llms.txt" rel="noopener noreferrer"&gt;https://www.usagegate.io/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>stripe</category>
      <category>api</category>
      <category>entitlements</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
