<?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: Dmitriy Malakhov</title>
    <description>The latest articles on DEV Community by Dmitriy Malakhov (@hennessy811).</description>
    <link>https://dev.to/hennessy811</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F816894%2F759b0061-0cd2-41a1-80b5-ee13b9295e84.jpeg</url>
      <title>DEV Community: Dmitriy Malakhov</title>
      <link>https://dev.to/hennessy811</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hennessy811"/>
    <language>en</language>
    <item>
      <title>3 Ways to Control Your SaaS Payments Without Opening a Dashboard</title>
      <dc:creator>Dmitriy Malakhov</dc:creator>
      <pubDate>Tue, 31 Mar 2026 15:05:17 +0000</pubDate>
      <link>https://dev.to/hennessy811/3-ways-to-control-your-saas-payments-without-opening-a-dashboard-20o2</link>
      <guid>https://dev.to/hennessy811/3-ways-to-control-your-saas-payments-without-opening-a-dashboard-20o2</guid>
      <description>&lt;p&gt;I manage a SaaS on &lt;a href="https://creem.io" rel="noopener noreferrer"&gt;Creem&lt;/a&gt;. I used to have the dashboard open all day — checking subscriptions, looking up failed payments, generating checkout links. Now I don't open it at all.&lt;/p&gt;

&lt;p&gt;Last Tuesday at 11pm I got a Slack alert about a past-due subscription. Instead of opening a browser, logging in, and clicking through three screens, I typed &lt;code&gt;creem subs list --status past_due --json | jq '.items[] | {email: .customer.email, product: .product.name}'&lt;/code&gt; and had the answer in 2 seconds. Then I paused the subscription, sent the customer an email, and went back to sleep. All from the terminal.&lt;/p&gt;

&lt;p&gt;The Creem CLI gives you three increasingly powerful ways to manage payments: direct commands for quick lookups, &lt;code&gt;--json | jq&lt;/code&gt; pipelines for analytics and automation, and an MCP server that lets Claude or Cursor manage your store through natural language. Here's the full playbook — with a &lt;a href="https://github.com/malakhov-dmitrii/creem-cli-toolkit" rel="noopener noreferrer"&gt;ready-to-use toolkit repo&lt;/a&gt; containing 9 shell scripts, MCP configs, and a Claude Code skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup (2 minutes)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap armitage-labs/creem
brew &lt;span class="nb"&gt;install &lt;/span&gt;creem
creem login &lt;span class="nt"&gt;--api-key&lt;/span&gt; creem_test_xxx
creem &lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch between test and live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem config &lt;span class="nb"&gt;set &lt;/span&gt;environment &lt;span class="nb"&gt;test&lt;/span&gt;   &lt;span class="c"&gt;# test-api.creem.io&lt;/span&gt;
creem config &lt;span class="nb"&gt;set &lt;/span&gt;environment live   &lt;span class="c"&gt;# api.creem.io&lt;/span&gt;
creem config show                   &lt;span class="c"&gt;# verify current state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 1: Direct Commands
&lt;/h2&gt;

&lt;p&gt;Every resource — products, subscriptions, customers, transactions, discounts — is a subcommand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem products list              &lt;span class="c"&gt;# Table view&lt;/span&gt;
creem subs list &lt;span class="nt"&gt;--status&lt;/span&gt; active  &lt;span class="c"&gt;# Filter subscriptions&lt;/span&gt;
creem customers list             &lt;span class="c"&gt;# All customers&lt;/span&gt;
creem txn list &lt;span class="nt"&gt;--limit&lt;/span&gt; 5         &lt;span class="c"&gt;# Recent transactions&lt;/span&gt;
creem discounts get &lt;span class="nt"&gt;--code&lt;/span&gt; LAUNCH20  &lt;span class="c"&gt;# Look up a discount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run any resource command without a subcommand for an interactive TUI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem products     &lt;span class="c"&gt;# Navigate with j/k, select with Enter&lt;/span&gt;
creem subs         &lt;span class="c"&gt;# Same for subscriptions&lt;/span&gt;
creem customers    &lt;span class="c"&gt;# Browse and inspect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TUI is keyboard-driven — &lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; to scroll, &lt;code&gt;/&lt;/code&gt; to search, Enter to view details. Perfect for quick lookups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Resources
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# New product&lt;/span&gt;
creem products create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Pro Plan"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--price&lt;/span&gt; 2900 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--billing-type&lt;/span&gt; recurring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--billing-period&lt;/span&gt; every-month

&lt;span class="c"&gt;# Checkout link (copy to clipboard)&lt;/span&gt;
creem checkouts create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--product&lt;/span&gt; prod_xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--success-url&lt;/span&gt; &lt;span class="s2"&gt;"https://myapp.com/thanks"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.checkoutUrl'&lt;/span&gt; | pbcopy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Subscription Lifecycle
&lt;/h3&gt;

&lt;p&gt;The full lifecycle without opening a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem subs list &lt;span class="nt"&gt;--status&lt;/span&gt; active       &lt;span class="c"&gt;# Who's paying?&lt;/span&gt;
creem subs get sub_xxx                &lt;span class="c"&gt;# Details + billing dates&lt;/span&gt;
creem subs pause sub_xxx              &lt;span class="c"&gt;# Pause billing&lt;/span&gt;
creem subs resume sub_xxx             &lt;span class="c"&gt;# Resume&lt;/span&gt;
creem subs cancel sub_xxx             &lt;span class="c"&gt;# Cancel (end of period)&lt;/span&gt;
creem subs cancel sub_xxx &lt;span class="nt"&gt;--mode&lt;/span&gt; immediate  &lt;span class="c"&gt;# Cancel now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 2: &lt;code&gt;--json | jq&lt;/code&gt; Pipelines
&lt;/h2&gt;

&lt;p&gt;Every command supports &lt;code&gt;--json&lt;/code&gt;. The response shape is always &lt;code&gt;{ items: [...] }&lt;/code&gt;. This makes the CLI fully composable with jq, shell scripts, and cron jobs.&lt;/p&gt;

&lt;p&gt;This is where the terminal approach starts beating the dashboard. You can't &lt;code&gt;GROUP BY product&lt;/code&gt; in a web UI. You can't pipe dashboard results into a Slack webhook. You can't run your revenue report at 9am every day automatically. With &lt;code&gt;--json | jq&lt;/code&gt;, all of that is one line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calculate MRR&lt;/strong&gt; — the number every SaaS founder checks first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem subs list &lt;span class="nt"&gt;--status&lt;/span&gt; active &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'
  [.items[] | .product.price] | add / 100
'&lt;/span&gt;
&lt;span class="c"&gt;# 125&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Revenue by product:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem txn list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'
  [.items[] | select(.status=="paid")]
  | group_by(.description)
  | map({
      product: .[0].description,
      total: ([.[].amount] | add / 100),
      count: length
    })
  | sort_by(-.total)
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Customers by country:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem customers list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'
  [.items[] | .country]
  | group_by(.) | map({country: .[0], count: length})
  | sort_by(-.count)
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;High-value transactions (over $20):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem txn list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'
  [.items[] | select(.amount &amp;gt; 2000)]
  | map({id: .id, amount: (.amount/100), status: .status})
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Product pricing table (tab-separated for spreadsheets):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem products list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  .items[] | [.name, "$\(.price/100)", .billingPeriod] | @tsv
'&lt;/span&gt;
&lt;span class="c"&gt;# Starter    $9     every-month&lt;/span&gt;
&lt;span class="c"&gt;# Pro        $29    every-month&lt;/span&gt;
&lt;span class="c"&gt;# Enterprise $99    every-month&lt;/span&gt;
&lt;span class="c"&gt;# Lifetime   $49    once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find all past-due subscriptions with customer emails&lt;/strong&gt; — this is the query I ran at 11pm on a Tuesday instead of opening the dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem subs list &lt;span class="nt"&gt;--status&lt;/span&gt; past_due &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'
  .items[] | {sub: .id, email: .customer.email, product: .product.name, since: .currentPeriodEndDate}
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bulk generate checkout links for all products:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem products list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.items[] | .id'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;pid&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;creem checkouts create &lt;span class="nt"&gt;--product&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--success-url&lt;/span&gt; &lt;span class="s2"&gt;"https://myapp.com/thanks"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.checkoutUrl'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;creem products get &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Daily revenue cron job:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Add to crontab: 0 9 * * * ~/daily-revenue.sh&lt;/span&gt;
&lt;span class="nv"&gt;REVENUE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;creem txn list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'[.items[] | select(.status=="paid") | .amount] | add / 100'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;SUBS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;creem subs list &lt;span class="nt"&gt;--status&lt;/span&gt; active &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.items | length'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; | Revenue: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="nv"&gt;$REVENUE&lt;/span&gt;&lt;span class="s2"&gt; | Active subs: &lt;/span&gt;&lt;span class="nv"&gt;$SUBS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/creem-daily.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Count subscriptions by status:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;status &lt;span class="k"&gt;in &lt;/span&gt;active trialing past_due paused canceled&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;creem subs list &lt;span class="nt"&gt;--status&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.items | length'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="nv"&gt;$COUNT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 3: AI Agent via MCP Server
&lt;/h2&gt;

&lt;p&gt;This is where it gets wild. The &lt;code&gt;creem&lt;/code&gt; npm package ships with an MCP server — 22 tools that any MCP-compatible AI agent can call directly via the Creem SDK. This is not a wrapper that shells out to the CLI. The MCP server calls the Creem SDK directly, which means it can do things the CLI can't — create discounts, manage licenses, upgrade subscriptions.&lt;/p&gt;

&lt;p&gt;The key insight: other CLI-to-MCP solutions parse text output from shell commands. This one is SDK-native. The AI gets structured data and typed tool definitions, not string-munged terminal output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup for Claude Code / Claude Desktop:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add to your MCP config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"creem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--package"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"creem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="s2"&gt;"--api-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"creem_test_xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="s2"&gt;"--server-index"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cursor, same format in &lt;code&gt;.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the AI can do:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You say&lt;/th&gt;
&lt;th&gt;MCP tool called&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Show me all active subscriptions"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subscriptions-search-subscriptions&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Create a 20% discount for the Pro plan"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;discounts-create&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Generate a checkout link for Enterprise"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;checkouts-create&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Pause Bob's subscription"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subscriptions-pause&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Activate this license key"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;licenses-activate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Upgrade to the Enterprise plan"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;subscriptions-upgrade&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MCP server exposes tools the CLI doesn't have. &lt;code&gt;discounts-create&lt;/code&gt; and &lt;code&gt;discounts-delete&lt;/code&gt; are MCP-only — the CLI can read discounts but can't create them. &lt;code&gt;licenses-activate/deactivate/validate&lt;/code&gt; and &lt;code&gt;subscriptions-upgrade&lt;/code&gt; are also MCP-only. Through MCP, your AI agent has full CRUD access to your store.&lt;/p&gt;

&lt;p&gt;All 22 tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products-get, products-create, products-search
customers-list, customers-retrieve, customers-generate-billing-links
subscriptions-get, subscriptions-cancel, subscriptions-update,
  subscriptions-upgrade, subscriptions-pause, subscriptions-resume,
  subscriptions-search-subscriptions
checkouts-retrieve, checkouts-create
licenses-activate, licenses-deactivate, licenses-validate
discounts-get, discounts-create, discounts-delete
transactions-get-by-id, transactions-search
stats-get-metrics-summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creem is one of the first payment platforms with native agent support. Their onboarding spec at &lt;a href="https://creem.io/SKILL.md" rel="noopener noreferrer"&gt;creem.io/SKILL.md&lt;/a&gt; describes 7 routing workflows for AI agents — from selling products to managing subscriptions to handling support tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Migrate from Lemon Squeezy
&lt;/h2&gt;

&lt;p&gt;Switching? One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem migrate lemon-squeezy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preview first with &lt;code&gt;--dry-run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem migrate lemon-squeezy &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or export the migration plan as JSON for review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem migrate lemon-squeezy &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; migration-plan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip discounts if you want to recreate them manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;creem migrate lemon-squeezy &lt;span class="nt"&gt;--exclude-discounts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wizard maps your products, pricing, and billing periods. No CSV exports, no manual data entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use What
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;CLI&lt;/th&gt;
&lt;th&gt;JSON + jq&lt;/th&gt;
&lt;th&gt;MCP Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quick lookup&lt;/td&gt;
&lt;td&gt;&lt;code&gt;creem subs get id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;"Show sub X"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk analysis&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jq 'group_by...'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"Analyze churn"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell scripts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;creem txn list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pipe to jq&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;CLI commands&lt;/td&gt;
&lt;td&gt;parse output&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout links&lt;/td&gt;
&lt;td&gt;&lt;code&gt;creem checkouts create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jq .url&lt;/code&gt; → pbcopy&lt;/td&gt;
&lt;td&gt;"Create checkout"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue metrics&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;jq math&lt;/td&gt;
&lt;td&gt;&lt;code&gt;transactions-search&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create discounts&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;discounts-create&lt;/code&gt; (MCP-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License management&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;licenses-activate/validate&lt;/code&gt; (MCP-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;creem migrate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--json &amp;gt; plan.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three interfaces, one payment platform. Use each where it's strongest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grab the Toolkit
&lt;/h2&gt;

&lt;p&gt;All the scripts, MCP configs, and the Claude Code skill from this article are packaged in a ready-to-use repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/malakhov-dmitrii/creem-cli-toolkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;9 shell scripts&lt;/strong&gt; — MRR, revenue by product, sub health, checkout links, daily cron, bulk ops, past-due alerts, customers by country, pricing table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP configs&lt;/strong&gt; — copy-paste JSON for Claude Code, Cursor, and Claude Desktop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code skill&lt;/strong&gt; — &lt;code&gt;SKILL.md&lt;/code&gt; with full CLI reference and safety rules (install via &lt;code&gt;clawhub install creem-cli-toolkit&lt;/code&gt; or copy to &lt;code&gt;.claude/skills/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo script&lt;/strong&gt; — step-through version of all 3 levels for live demos&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap armitage-labs/creem &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install &lt;/span&gt;creem
creem login
creem products list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terminal to payments in 2 minutes. No dashboard required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Video walkthrough:&lt;/strong&gt; &lt;a href="https://youtu.be/td6hwGfLvxQ" rel="noopener noreferrer"&gt;youtube.com/watch?v=td6hwGfLvxQ&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://creem.io/scoops" rel="noopener noreferrer"&gt;Creem Scoops&lt;/a&gt; bounty program. CLI v0.1.3, MCP server via &lt;a href="https://www.npmjs.com/package/creem" rel="noopener noreferrer"&gt;creem&lt;/a&gt; v1.4.4.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>ai</category>
      <category>saas</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a Complete SaaS with Next.js, Supabase &amp; Creem - Subscriptions, Credits, Licenses in 30 Minutes</title>
      <dc:creator>Dmitriy Malakhov</dc:creator>
      <pubDate>Wed, 25 Mar 2026 20:45:16 +0000</pubDate>
      <link>https://dev.to/hennessy811/build-a-complete-saas-with-nextjs-supabase-creem-subscriptions-credits-licenses-in-30-4jmb</link>
      <guid>https://dev.to/hennessy811/build-a-complete-saas-with-nextjs-supabase-creem-subscriptions-credits-licenses-in-30-4jmb</guid>
      <description>&lt;p&gt;So you want to launch a SaaS product. You need auth, a database, payments, subscription management, license keys, a credits system, discount codes, webhooks... and it all needs to work together.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;SaaSKit&lt;/strong&gt; - an open-source boilerplate that wires all of this up with Next.js 16, Supabase, and Creem. In this tutorial, I'll walk you through how it works and how to get your own SaaS running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://nextjs-supabase-creem-boilerplate.vercel.app/" rel="noopener noreferrer"&gt;https://nextjs-supabase-creem-boilerplate.vercel.app/&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate" rel="noopener noreferrer"&gt;https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxktozorou7w5ntdww414.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxktozorou7w5ntdww414.png" alt="Boilerplate landing screenshot"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Creem?
&lt;/h2&gt;

&lt;p&gt;If you've used Stripe, you know the pain: tax compliance, VAT handling, international regulations. Creem is a &lt;strong&gt;Merchant of Record&lt;/strong&gt; - they handle all of that for you. 3.9% + 30c, no monthly fees, and a TypeScript SDK that's a joy to use.&lt;/p&gt;

&lt;p&gt;They also have built-in license key management, which is rare for payment processors.&lt;/p&gt;


&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;p&gt;SaaSKit ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; -- Email/password + Google/GitHub OAuth via Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3-tier pricing&lt;/strong&gt; -- Starter ($9), Pro ($29), Enterprise ($99)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscription management&lt;/strong&gt; -- Upgrade, downgrade, cancel (scheduled or immediate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seat-based billing&lt;/strong&gt; -- Add/remove team members&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 webhook events&lt;/strong&gt; -- Full subscription lifecycle, refunds, disputes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License keys&lt;/strong&gt; -- Activate, validate, deactivate per device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credits wallet&lt;/strong&gt; -- Atomic Postgres operations, no race conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discount codes&lt;/strong&gt; -- Percentage or fixed amount, product-scoped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email notifications&lt;/strong&gt; -- Welcome + payment confirmation via Resend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; -- Upstash Redis on sensitive routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO&lt;/strong&gt; -- Sitemap, robots.txt, OG image generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo mode&lt;/strong&gt; -- Works without any accounts, pre-seeded with data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;131 tests&lt;/strong&gt; -- All importing from actual source code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt; -- GitHub Actions pipeline&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Quick Start (Demo Mode)
&lt;/h2&gt;

&lt;p&gt;No accounts needed. Clone 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;git clone https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate saaskit
&lt;span class="nb"&gt;cd &lt;/span&gt;saaskit
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;. Demo mode activates automatically - you'll see a pre-seeded Pro subscription, 50 credits, and a sample license key. The full checkout flow works with in-memory data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The project uses Next.js 16 App Router with a clean structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  app/
    (auth)/          -- Login, signup, OAuth callback
    api/             -- 11 API routes (checkout, subscriptions, licenses, credits, etc.)
    dashboard/       -- Main dashboard, transactions, licenses, admin
    pricing/         -- Pricing page with discount code support
  components/        -- 14 client components
  lib/
    creem.ts         -- SDK client with auto test/prod detection
    email.ts         -- Resend integration with graceful fallback
    rate-limit.ts    -- Upstash rate limiting
    demo/            -- Demo mode: store, mock, detection
    supabase/        -- Browser, server, admin clients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setting Up Payments with Creem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install the SDK
&lt;/h3&gt;

&lt;p&gt;SaaSKit uses both the Creem SDK and the Next.js webhook adapter:&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;install &lt;/span&gt;creem @creem_io/nextjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create a Checkout
&lt;/h3&gt;

&lt;p&gt;The checkout route creates a Creem hosted checkout session:&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;// src/app/api/checkout/route.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;creem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkouts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;successUrl&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;appUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/dashboard?checkout=success`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;discountCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;discountCode&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user_id&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="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkoutUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;metadata.user_id&lt;/code&gt; is critical - it's how webhooks know which user to update.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Handle Webhooks
&lt;/h3&gt;

&lt;p&gt;SaaSKit handles all 13 Creem webhook events using &lt;code&gt;@creem_io/nextjs&lt;/code&gt;:&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;// src/app/api/webhooks/creem/route.ts&lt;/span&gt;
&lt;span class="nc"&gt;Webhook&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;CREEM_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;onCheckoutCompleted&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;event&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;// Upsert subscription, send confirmation email&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onSubscriptionCanceled&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;event&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;// Update status to cancelled&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onGrantAccess&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;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metadata&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;// Set subscription active&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onRevokeAccess&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;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metadata&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;// Revoke access based on reason&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 9 more handlers&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every webhook handler includes idempotency checking via a &lt;code&gt;webhook_events&lt;/code&gt; table - duplicate events are detected and skipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Credits System
&lt;/h2&gt;

&lt;p&gt;The credits wallet uses atomic Postgres operations to prevent race conditions:&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="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;spend_credits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_amount&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p_reason&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="c1"&gt;-- Lock row, check balance (handles unlimited sentinel -1)&lt;/span&gt;
  &lt;span class="c1"&gt;-- Deduct and log transaction atomically&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enterprise plans get unlimited credits (stored as &lt;code&gt;-1&lt;/code&gt; sentinel). The app layer checks &lt;code&gt;isUnlimited()&lt;/code&gt; before calling the RPC, and the SQL function handles it as a defense-in-depth measure.&lt;/p&gt;




&lt;h2&gt;
  
  
  License Key Management
&lt;/h2&gt;

&lt;p&gt;Creem has built-in license key support. SaaSKit exposes three API routes:&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;// Activate a license on a device&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;creem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;licenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;instanceName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Validate it's still active&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;creem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;licenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Deactivate when done&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;creem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;licenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;instanceId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;License keys are delivered automatically via the &lt;code&gt;checkout.completed&lt;/code&gt; webhook and displayed in the dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo Mode
&lt;/h2&gt;

&lt;p&gt;One of SaaSKit's unique features is a full demo mode that works without any external services:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDemoMode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;url&lt;/span&gt; &lt;span class="o"&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;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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;PLACEHOLDER_URLS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When no real Supabase URL is configured, the app uses an in-memory store with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pre-seeded Pro subscription&lt;/li&gt;
&lt;li&gt;50 credits with transaction history&lt;/li&gt;
&lt;li&gt;A sample active license key&lt;/li&gt;
&lt;li&gt;Full mock Supabase client supporting selects, inserts, upserts, and RPC calls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means judges, reviewers, or potential users can clone the repo and see everything working immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;SaaSKit has &lt;strong&gt;131 unit tests&lt;/strong&gt; across 19 test files, plus Playwright E2E specs. Every test imports from the actual source module - no reimplemented logic.&lt;/p&gt;

&lt;p&gt;The key pattern: extract pure functions into &lt;code&gt;handlers.ts&lt;/code&gt;, &lt;code&gt;validators.ts&lt;/code&gt;, and &lt;code&gt;helpers.ts&lt;/code&gt; files, then test those directly:&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;// tests/api/webhook-handler.test.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;mapSubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buildSubscriptionUpsert&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;@/app/api/webhooks/creem/handlers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maps subscription.canceled to cancelled&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mapSubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription.canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deploying to Vercel
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Push to GitHub, then:&lt;/span&gt;
&lt;span class="c"&gt;# 1. Import repo in Vercel&lt;/span&gt;
&lt;span class="c"&gt;# 2. Add environment variables&lt;/span&gt;
&lt;span class="c"&gt;# 3. Deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deployment, configure your Creem webhook URL to &lt;code&gt;https://your-app.vercel.app/api/webhooks/creem&lt;/code&gt; and you're live.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;SaaSKit gives you a production-ready foundation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12 video proofs&lt;/strong&gt; demonstrating every flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;131 honest tests&lt;/strong&gt; with CI/CD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo mode&lt;/strong&gt; for instant evaluation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 webhook handlers&lt;/strong&gt; for full subscription lifecycle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting, email, SEO&lt;/strong&gt; out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is MIT licensed. Clone it, customize it, ship your SaaS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate" rel="noopener noreferrer"&gt;https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://nextjs-supabase-creem-boilerplate.vercel.app/" rel="noopener noreferrer"&gt;https://nextjs-supabase-creem-boilerplate.vercel.app/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the Creem Scoops bounty program. Creem is a Merchant of Record for SaaS - learn more at &lt;a href="https://creem.io" rel="noopener noreferrer"&gt;creem.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
