<?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>Forem: ilshaad</title>
    <description>The latest articles on Forem by ilshaad (@ilshadyx).</description>
    <link>https://forem.com/ilshadyx</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%2F3690402%2Fabb27eda-4dd7-4c0c-a408-2c21ee0b99b0.png</url>
      <title>Forem: ilshaad</title>
      <link>https://forem.com/ilshadyx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ilshadyx"/>
    <language>en</language>
    <item>
      <title>How to Calculate MRR, Churn, and LTV in PostgreSQL</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Mon, 13 Apr 2026 11:02:22 +0000</pubDate>
      <link>https://forem.com/ilshadyx/how-to-calculate-mrr-churn-and-ltv-in-postgresql-2oan</link>
      <guid>https://forem.com/ilshadyx/how-to-calculate-mrr-churn-and-ltv-in-postgresql-2oan</guid>
      <description>&lt;p&gt;Every SaaS founder eventually hits the same wall. Payments are flowing, subscriptions are growing, and your billing provider's dashboard gives you a decent overview. But then someone asks: "What's our MRR trend over the last 6 months?" or "What's our average customer lifetime value?" — and suddenly you're exporting CSVs or clicking through UI tabs trying to piece it together.&lt;/p&gt;

&lt;p&gt;Whether you're using Stripe, Paddle, QuickBooks, or any other billing provider, their dashboards show you what happened. But the metrics that actually drive SaaS decisions — MRR, churn rate, LTV, net revenue retention — require querying your billing data flexibly. Joining it with your own application data. Slicing it by cohort, plan, or time period. That's not something any dashboard gives you.&lt;/p&gt;

&lt;p&gt;Most metric guides give you formulas. This one gives you copy-paste SQL that works on real billing tables — whether your data comes from Stripe, Paddle, QuickBooks, or any other provider. Get your billing data into PostgreSQL, run these queries, and you'll have a clearer picture of your SaaS than any third-party analytics tool can provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL for SaaS Metrics?
&lt;/h2&gt;

&lt;p&gt;Your billing provider's dashboard shows you what happened. PostgreSQL lets you ask why.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom time ranges&lt;/strong&gt; — monthly, weekly, daily, or any arbitrary period. Not limited to what your billing UI offers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JOIN with your app data&lt;/strong&gt; — correlate billing events with user behavior, feature usage, or support tickets. Which plan tier has the lowest churn? Which signup source has the highest LTV? You can only answer these by joining billing data with your own tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cohort analysis&lt;/strong&gt; — group customers by signup month and track how each cohort retains over time. This is nearly impossible through an API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limits&lt;/strong&gt; — run the same query a hundred times while tweaking your analysis. No API throttling, no pagination, no waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shareable and reproducible&lt;/strong&gt; — SQL queries can be saved, version-controlled, and rerun. They become your team's source of truth for metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Your Billing Data into PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Before you can query anything, you need your billing data in a database. You can build a custom integration (handle API polling, pagination, schema mapping, and ongoing maintenance) or use a tool that does it for you.&lt;/p&gt;

&lt;p&gt;Here's the quick setup using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connect your database&lt;/strong&gt; — paste your PostgreSQL connection string (works with Supabase, Neon, Railway, Render, AWS RDS, or any Postgres host)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select your provider&lt;/strong&gt; — choose Stripe, Paddle, QuickBooks, Xero, or another supported billing provider and pick a data type (customers, subscriptions, transactions, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add your API key&lt;/strong&gt; — generate a read-only key from your provider's developer settings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the table&lt;/strong&gt; — click Auto-Create Table to generate the correct schema automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync&lt;/strong&gt; — hit Sync Now and your data appears as a regular Postgres table&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set up a scheduled sync (hourly or daily) and the data stays current automatically. Subsequent syncs are incremental — only changed records are fetched.&lt;/p&gt;

&lt;p&gt;You'll want to sync at least &lt;strong&gt;customers&lt;/strong&gt;, &lt;strong&gt;subscriptions&lt;/strong&gt;, and &lt;strong&gt;transactions&lt;/strong&gt; to run the queries below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The SQL queries below use generic table names (&lt;code&gt;subscriptions&lt;/code&gt;, &lt;code&gt;customers&lt;/code&gt;, &lt;code&gt;transactions&lt;/code&gt;). Your actual table names will depend on your billing provider — for example, &lt;code&gt;stripe_subscriptions&lt;/code&gt;, &lt;code&gt;paddle_subscriptions&lt;/code&gt;, etc. Column names may also vary slightly between providers. Adjust as needed for your schema.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  MRR (Monthly Recurring Revenue)
&lt;/h2&gt;

&lt;p&gt;MRR is the baseline metric for any subscription business. Here's how to calculate it from your billing data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current MRR — what you're earning right now:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;active_subscriptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currency_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'month'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'year'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;currency_code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MRR trend over the last 12 months:&lt;/strong&gt;&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;WITH&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt; &lt;span class="k"&gt;AS&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'11 months'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="s1"&gt;'1 month'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;monthly_mrr&lt;/span&gt; &lt;span class="k"&gt;AS&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'month'&lt;/span&gt;
          &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'year'&lt;/span&gt;
          &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 month'&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'canceled'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency_code&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mrr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mrr&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;monthly_mrr&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a clear picture of how revenue is trending — are you growing, plateauing, or declining? It accounts for both monthly and annual subscriptions by normalizing annual plans to their monthly equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Churn Rate
&lt;/h2&gt;

&lt;p&gt;Churn tells you how fast you're losing customers. A small difference in churn rate compounds massively over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly churn rate:&lt;/strong&gt;&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;WITH&lt;/span&gt; &lt;span class="n"&gt;monthly_stats&lt;/span&gt; &lt;span class="k"&gt;AS&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;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canceled_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;churned&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'canceled'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'12 months'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;monthly_active&lt;/span&gt; &lt;span class="k"&gt;AS&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;active_at_start&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'11 months'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="s1"&gt;'1 month'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_at_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;churned&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;churned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;churned&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;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_at_start&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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;churn_rate_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;monthly_active&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;monthly_stats&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SaaS benchmarks: under 5% monthly churn is decent, under 3% is good, under 1% is excellent. If you're above 5%, this is the first metric to focus on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customer Lifetime Value (LTV)
&lt;/h2&gt;

&lt;p&gt;LTV tells you how much revenue a customer generates on average before they leave. It's essential for understanding how much you can spend on acquisition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Average LTV across all customers:&lt;/strong&gt;&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;WITH&lt;/span&gt; &lt;span class="n"&gt;customer_revenue&lt;/span&gt; &lt;span class="k"&gt;AS&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;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_transaction&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;total_customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_ltv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PERCENTILE_CONT&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;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;WITHIN&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;median_ltv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_ltv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOCH&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_transaction&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;first_transaction&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_lifetime_days&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_revenue&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt; &lt;span class="o"&gt;&amp;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;&lt;strong&gt;LTV by signup cohort — are newer customers more or less valuable?&lt;/strong&gt;&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;WITH&lt;/span&gt; &lt;span class="n"&gt;customer_revenue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cohort_month&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;cohort_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_ltv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_cohort_revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_revenue&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cohort_month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cohort_month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If LTV is trending down across cohorts, it could mean you're attracting less committed customers, or that your pricing needs adjustment. If it's trending up, your product is getting stickier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Net Revenue Retention (NRR)
&lt;/h2&gt;

&lt;p&gt;NRR tells you whether your existing customers are spending more or less over time. Over 100% means expansion revenue outpaces churn — the gold standard for SaaS.&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;WITH&lt;/span&gt; &lt;span class="n"&gt;monthly_cohort_revenue&lt;/span&gt; &lt;span class="k"&gt;AS&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;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;retention&lt;/span&gt; &lt;span class="k"&gt;AS&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;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 month'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revenue&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_revenue&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;monthly_cohort_revenue&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;
  &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;monthly_cohort_revenue&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 month'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;previous_month_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_month_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_revenue&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous_revenue&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;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nrr_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;retention&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NRR above 100% means your existing customers are growing. Below 100% means you're leaking revenue from your current base — even if new sales look healthy, the bucket has a hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: New vs Churned MRR
&lt;/h2&gt;

&lt;p&gt;This shows where your MRR growth is coming from — and whether new revenue is outpacing losses.&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;WITH&lt;/span&gt; &lt;span class="n"&gt;new_mrr&lt;/span&gt; &lt;span class="k"&gt;AS&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;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'month'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'year'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_mrr&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'12 months'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;churned_mrr&lt;/span&gt; &lt;span class="k"&gt;AS&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;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canceled_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'month'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;billing_cycle_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'year'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;recurring_amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lost_mrr&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;canceled_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'12 months'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'canceled'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_mrr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;new_mrr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lost_mrr&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;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;churned_mrr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_mrr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lost_mrr&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;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;net_mrr_change&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;new_mrr&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;churned_mrr&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;net_mrr_change&lt;/code&gt; is consistently positive, you're growing. If churned MRR regularly exceeds new MRR, you've got a retention problem that no amount of marketing will fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping Your Metrics Current
&lt;/h2&gt;

&lt;p&gt;These queries are only as good as the data behind them. Stale data means stale metrics.&lt;/p&gt;

&lt;p&gt;With scheduled syncs running hourly or daily, your billing tables stay current automatically. Each sync is incremental — only changed records are fetched — so it's fast and stays well within your provider's rate limits.&lt;/p&gt;

&lt;p&gt;Your SaaS metrics dashboard becomes a set of saved SQL queries that always return up-to-date numbers. No manual exports, no API scripts to maintain, no data pipelines to babysit.&lt;/p&gt;




&lt;p&gt;Ready to get your billing data into PostgreSQL? &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt; has a free tier to get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is MRR and how do you calculate it?
&lt;/h3&gt;

&lt;p&gt;MRR (Monthly Recurring Revenue) is the total predictable revenue your subscription business earns each month. To calculate it, sum the monthly value of all active subscriptions — converting annual plans to their monthly equivalent by dividing by 12. In PostgreSQL, you can query this directly from your billing data using the SQL examples in this guide. Tools like &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; keep your subscription data current so MRR calculations always reflect the latest state.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a good churn rate for SaaS?
&lt;/h3&gt;

&lt;p&gt;For SaaS businesses, under 5% monthly customer churn is considered acceptable, under 3% is good, and under 1% is excellent. Early-stage startups often see higher churn (5-10%) as they find product-market fit. The key is tracking churn consistently over time — which requires your billing data in a queryable database rather than relying on dashboard snapshots.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you calculate customer lifetime value in SQL?
&lt;/h3&gt;

&lt;p&gt;Customer lifetime value (LTV) is the total revenue a customer generates before they leave. The simplest SQL approach is to sum all completed transactions per customer and take the average. For a more nuanced view, calculate LTV by signup cohort to see whether newer customers are more or less valuable than earlier ones. Both queries are included in this guide and work with any billing provider's data synced to PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is net revenue retention and why does it matter?
&lt;/h3&gt;

&lt;p&gt;Net Revenue Retention (NRR) measures whether your existing customers are spending more or less over time, expressed as a percentage. NRR above 100% means expansion revenue (upgrades, add-ons) outpaces churn and contractions — your revenue grows even without new customers. Below 100% means you're losing revenue from your existing base. Top-performing SaaS companies aim for 110-130% NRR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to build a data pipeline to run these queries?
&lt;/h3&gt;

&lt;p&gt;No. You can get your billing data into PostgreSQL in about 5 minutes using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; — connect your database, select your billing provider (Stripe, Paddle, QuickBooks, or Xero), and the tables are created and populated automatically. Scheduled syncs keep the data current so your metric queries always return up-to-date numbers without any pipeline maintenance.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-quickbooks-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync QuickBooks Data to PostgreSQL Automatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-xero-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Xero to PostgreSQL Automatically in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-billing-data-to-aws-rds-postgresql" rel="noopener noreferrer"&gt;How to Sync Your Billing Data to AWS RDS PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/best-stripe-sigma-alternative-for-postgresql" rel="noopener noreferrer"&gt;Best Stripe Sigma Alternative for PostgreSQL Users&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;Connect Your Database to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>postgres</category>
      <category>saas</category>
      <category>sql</category>
    </item>
    <item>
      <title>How to Sync Xero to PostgreSQL Automatically in 5 Minutes</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Mon, 06 Apr 2026 10:22:52 +0000</pubDate>
      <link>https://forem.com/ilshadyx/how-to-sync-xero-to-postgresql-automatically-in-5-minutes-30be</link>
      <guid>https://forem.com/ilshadyx/how-to-sync-xero-to-postgresql-automatically-in-5-minutes-30be</guid>
      <description>&lt;p&gt;&lt;em&gt;Automate Xero to PostgreSQL sync — skip the OAuth, rate limits, and pipeline maintenance. Set it up once, keep your accounting data fresh forever.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 6 Apr 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you run your accounting on Xero and your application on PostgreSQL, you've probably wanted both datasets in the same place. The problem is Xero's API wasn't designed for that. Between the OAuth handshake, organisation selection, aggressive rate limits, and token lifecycle, building a reliable sync pipeline takes longer than most teams expect — and maintaining it takes even longer.&lt;/p&gt;

&lt;p&gt;An AI can generate the initial integration code in an afternoon. What it can't do is keep it running: refreshing tokens before they expire, handling the multi-tenant connection flow, respecting the 60-call-per-minute cap, recovering from failures at 3am. That's where the real cost lives — not in the build, but in the upkeep.&lt;/p&gt;

&lt;p&gt;This guide covers a five-minute alternative. Connect your database, authorize your Xero organisation, and your contacts, invoices, or bank transactions show up as a queryable Postgres table — kept in sync automatically on whatever schedule you choose.&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%2F0c5p2kkg7t3lb1w914pu.jpg" 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%2F0c5p2kkg7t3lb1w914pu.jpg" alt="Sync Xero accounting data to PostgreSQL automatically with Codeless Sync" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, though, it helps to understand what makes the Xero API tricky to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Xero API Is Painful
&lt;/h2&gt;

&lt;p&gt;Xero's API is powerful, but it wasn't built for bulk data extraction. Even experienced developers hit walls quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0 with mandatory tenant selection.&lt;/strong&gt; There's no simple API key. Every Xero integration requires a full &lt;a href="https://developer.xero.com/documentation/guides/oauth2/auth-flow/" rel="noopener noreferrer"&gt;OAuth 2.0 flow&lt;/a&gt; — registering an app in the Xero Developer portal, handling authorization redirects, storing tokens, and refreshing them before they expire. But Xero adds an extra layer: after authorization, you must query the &lt;a href="https://developer.xero.com/documentation/guides/oauth2/tenants" rel="noopener noreferrer"&gt;&lt;code&gt;/connections&lt;/code&gt; endpoint&lt;/a&gt; to list the user's organisations, then let them select which one to sync. If you skip this, you can't make any API calls. Miss a token refresh and your integration silently stops working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strict rate limits.&lt;/strong&gt; Xero enforces &lt;a href="https://developer.xero.com/documentation/guides/oauth2/limits/" rel="noopener noreferrer"&gt;60 API calls per minute per tenant and 5,000 calls per day&lt;/a&gt; per app. That's far tighter than most billing APIs. Hit the limit and you're throttled — your sync stalls until the window resets. Building retry logic and backoff handling is mandatory, not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No bulk export endpoint.&lt;/strong&gt; Want all your invoices? You're paginating through them page by page — the default is 100 records per page. Xero uses page-based pagination, so you're making repeated calls, tracking page numbers, and handling edge cases when records are added mid-pagination. For large accounts with thousands of contacts or invoices, this means dozens of API calls just to get a complete dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex accounting relationships.&lt;/strong&gt; Xero entities have deep relationships. An invoice references a contact, line items reference accounts, payments reference invoices and bank accounts. Getting a complete picture means querying multiple endpoints and joining the results yourself.&lt;/p&gt;

&lt;p&gt;Most teams that need Xero data in PostgreSQL end up in one of three places:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom integration.&lt;/strong&gt; Build an OAuth flow with tenant selection, write polling logic, design table schemas, handle token refresh, manage error recovery. AI can generate the boilerplate fast, but you're still maintaining it — fixing edge cases when tokens expire, handling schema changes when Xero updates their API, debugging silent failures when you hit the 60 req/min limit at 2am. The initial build isn't the problem. The ongoing maintenance is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise ETL tools.&lt;/strong&gt; Fivetran, Airbyte, and Stitch all offer Xero connectors — but they're built for data warehouses like Snowflake and BigQuery, not for application databases. If all you need is a live table in Postgres, you're over-engineering the solution and overpaying for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSV exports.&lt;/strong&gt; You can download reports from Xero as spreadsheets. That works for a one-off analysis, but it's manual, stale the moment you export it, and impossible to automate into your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL for Accounting Data?
&lt;/h2&gt;

&lt;p&gt;Once your Xero data lives in PostgreSQL, your accounting stops being a separate system and becomes part of your application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-system JOINs&lt;/strong&gt; — connect Xero contacts to your &lt;code&gt;users&lt;/code&gt; table by email, link invoices to internal orders, or reconcile payments against your own billing records. No API calls, just SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate limit ceiling&lt;/strong&gt; — Xero caps you at 60 requests per minute. A Postgres table has no such limit. Run the same report a hundred times while iterating on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native SQL reporting&lt;/strong&gt; — monthly revenue, overdue invoices, reconciled bank transactions, contact balances. Write the query once, schedule it, or plug it into your dashboard tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep historical analysis&lt;/strong&gt; — Xero's API favours recent data. A synced table lets you &lt;code&gt;GROUP BY&lt;/code&gt; across years without pagination or API gymnastics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host-agnostic&lt;/strong&gt; — Supabase, Neon, Railway, AWS RDS, self-hosted — wherever your Postgres runs, that's where your Xero data lands.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five Steps to Automated Sync
&lt;/h2&gt;

&lt;p&gt;Here's the setup using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt;, which handles the OAuth flow, tenant selection, incremental syncing, token refresh, and schema mapping — so you set it up once and it just runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Connect Your Database
&lt;/h3&gt;

&lt;p&gt;In Codeless Sync, start by adding your PostgreSQL database. Paste your connection string — it works with any PostgreSQL host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://user:pass@your-host.example.com/dbname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection is tested automatically to make sure everything works before you proceed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick Your Data Type
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Create Sync Configuration&lt;/strong&gt; to open the wizard. Select &lt;strong&gt;Xero&lt;/strong&gt; as the provider and choose a data type. Available types include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contacts&lt;/strong&gt; — customers and suppliers with names, emails, phone numbers, and classifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoices&lt;/strong&gt; — sales invoices and bills with line items, amounts, and payment status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt; — payment records linked to invoices with amounts and dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounts&lt;/strong&gt; — your chart of accounts with account types and classifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bank Transactions&lt;/strong&gt; — money received and spent with reconciliation status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit Notes&lt;/strong&gt; — customer and supplier credits with remaining balances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Items&lt;/strong&gt; — products and services with codes, inventory tracking, and cost details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purchase Orders&lt;/strong&gt; — orders to suppliers with delivery dates and status tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Journals&lt;/strong&gt; — general ledger journal entries for audit trails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organisations&lt;/strong&gt; — company settings, currency, country, and tax configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contacts is a good starting point — it's easy to verify and gives you a feel for how the sync works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Authorize Xero
&lt;/h3&gt;

&lt;p&gt;Instead of building and maintaining an OAuth 2.0 flow with tenant selection, you click &lt;strong&gt;Connect to Xero&lt;/strong&gt; and authorize access through Xero's standard consent screen. After authorizing, you select which organisation to sync from — Xero supports multiple organisations per account, so you pick the one you need.&lt;/p&gt;

&lt;p&gt;Codeless Sync handles the developer app registration, redirect URIs, token storage, automatic token refresh, and tenant management behind the scenes — that's one less thing silently breaking in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create the Table
&lt;/h3&gt;

&lt;p&gt;Codeless Sync needs a destination table in your database. Click &lt;strong&gt;Auto-Create Table&lt;/strong&gt; and the correct schema is created automatically — columns matched to Xero fields, proper data types, and indexes for common queries.&lt;/p&gt;

&lt;p&gt;If you'd rather review the SQL first, copy the template and run it in your database client.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Verify Table&lt;/strong&gt; to confirm the structure is correct before proceeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Sync and Verify
&lt;/h3&gt;

&lt;p&gt;Name your configuration, click &lt;strong&gt;Create&lt;/strong&gt;, then hit &lt;strong&gt;Sync Now&lt;/strong&gt; from the dashboard. The first sync pulls all matching records from Xero. Depending on your data volume, this typically takes a few seconds to a couple of minutes.&lt;/p&gt;

&lt;p&gt;Once complete, open your database client and check:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_supplier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_date_utc&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;xero_contacts&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_date_utc&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see your Xero contacts, you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queries That Make This Worth It
&lt;/h2&gt;

&lt;p&gt;Syncing data is step one. The real payoff is what you can do with it once it's in Postgres. Here are some queries that become trivial once your Xero tables sit next to your application data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue from paid invoices by month:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;xero_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACCREC'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PAID'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Xero, &lt;code&gt;ACCREC&lt;/code&gt; means accounts receivable — invoices you've sent to customers. Filtering by &lt;code&gt;PAID&lt;/code&gt; gives you actual collected revenue, not just billed amounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outstanding invoices — who owes you money:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;invoice_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;contact_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount_due&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;days_overdue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;xero_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACCREC'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AUTHORISED'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;amount_due&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through the API, this would mean paginating all invoices, filtering for unpaid ones in your code, and computing overdue days manually — all while staying under the 60 req/min cap. In Postgres, it's six lines of SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Match Xero contacts to your application users:&lt;/strong&gt;&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;xero_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_supplier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_date_utc&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;xero_contacts&lt;/span&gt; &lt;span class="n"&gt;xc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email_address&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;xc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_date_utc&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app's user table and Xero's contact list, joined by email in a single query. That's the kind of cross-system insight that's impossible without both datasets in the same database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bank transaction summary by month:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;transaction_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;xero_bank_transactions&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_reconciled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reconciled bank transaction summary broken down by type (money received vs money spent). Try getting that from the API without burning through your daily rate limit.&lt;/p&gt;

&lt;p&gt;Every query above hits your local Postgres instance — zero API calls, zero rate limit concerns, instant results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set It and Forget It
&lt;/h2&gt;

&lt;p&gt;A one-time sync is useful. A pipeline that keeps itself running is transformative. Schedule syncs on an hourly, daily, or custom interval and your Xero tables stay current without any manual intervention.&lt;/p&gt;

&lt;p&gt;After the first full pull, every subsequent run is incremental — only fetching records modified since the last sync. This keeps each run fast and well inside Xero's 60 req/min and 5,000 req/day guardrails. No rate limit math required on your end.&lt;/p&gt;

&lt;p&gt;Token refresh, tenant connections, error recovery — all handled automatically. If something does fail, you're notified immediately instead of discovering stale data weeks later. It's production-grade data infrastructure without the maintenance overhead.&lt;/p&gt;

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

&lt;p&gt;Xero has the accounting data. PostgreSQL has your application data. Keeping them separate means API calls every time you need both — and building a custom pipeline to bridge the gap means owning OAuth, tenant management, rate limiting, and error handling forever.&lt;/p&gt;

&lt;p&gt;Five minutes of setup replaces all of that. Schedule the sync, and your Xero data becomes just another set of tables sitting next to the rest of your application — queryable, joinable, and always current.&lt;/p&gt;

&lt;p&gt;Give it a try: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you connect Xero to PostgreSQL?
&lt;/h3&gt;

&lt;p&gt;Yes. Xero's API exposes accounting data (contacts, invoices, payments, bank transactions, and more) that can be written to PostgreSQL tables. The challenge is building and maintaining the OAuth 2.0 flow, tenant selection, token refresh, and incremental polling logic. Tools like &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; handle the entire pipeline so you don't have to build it yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Xero data types can you sync to PostgreSQL?
&lt;/h3&gt;

&lt;p&gt;Xero exposes 10 core data types through its API: contacts, invoices, payments, accounts, bank transactions, credit notes, items, purchase orders, journals, and organisations. Each maps to its own PostgreSQL table (e.g. &lt;code&gt;xero_invoices&lt;/code&gt;, &lt;code&gt;xero_contacts&lt;/code&gt;) with columns matched to Xero's fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Xero have an API rate limit?
&lt;/h3&gt;

&lt;p&gt;Yes — and it's strict. Xero allows &lt;a href="https://developer.xero.com/documentation/guides/oauth2/limits/" rel="noopener noreferrer"&gt;60 API calls per minute, 5,000 per day, and 5 concurrent requests&lt;/a&gt; per connected organisation. These limits apply per tenant, not per app. Any sync solution needs to respect these caps, which is why incremental syncing (only fetching changed records) is essential for staying within limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Xero's API free to use?
&lt;/h3&gt;

&lt;p&gt;Xero's API is free for apps with up to 25 connected organisations. Beyond that, you need a &lt;a href="https://developer.xero.com/pricing" rel="noopener noreferrer"&gt;Xero App Partner plan&lt;/a&gt;. The API itself doesn't charge per call — the limits are rate-based (60/min, 5,000/day) rather than usage-based. Your costs come from whatever you build or use to consume the API, not from Xero itself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-quickbooks-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync QuickBooks Data to PostgreSQL Automatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;Connect Your Database to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xero</category>
      <category>postgres</category>
      <category>database</category>
      <category>automation</category>
    </item>
    <item>
      <title>Best Stripe Sigma Alternative for PostgreSQL Users</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Mon, 30 Mar 2026 10:55:52 +0000</pubDate>
      <link>https://forem.com/ilshadyx/best-stripe-sigma-alternative-for-postgresql-users-39fo</link>
      <guid>https://forem.com/ilshadyx/best-stripe-sigma-alternative-for-postgresql-users-39fo</guid>
      <description>&lt;p&gt;&lt;em&gt;Stripe Sigma locks your payment data inside Stripe and charges based on your monthly transaction volume. Here are better alternatives for PostgreSQL users — from automated sync tools to open-source ETL — compared side by side.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 30 Mar 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Stripe Sigma lets you query your payment data using SQL — directly inside the Stripe Dashboard. For quick ad-hoc queries on charges, subscriptions, or refunds, it works. But if you've ever wanted to join your Stripe data with your own app's user table, build a custom dashboard, or just run queries in your own database — you've hit Sigma's ceiling fast.&lt;/p&gt;

&lt;p&gt;Sigma uses tiered pricing based on your monthly charge volume — starting at $10/month plus $0.02 per charge for up to 500 charges, scaling to $50/month plus $0.016 per charge at higher volumes. A SaaS processing 5,000 charges per month is paying over $130/month just to query their own payment data. And the data never leaves Stripe — you can't export it to PostgreSQL, can't connect a BI tool directly, and can't combine it with anything outside Stripe's ecosystem.&lt;/p&gt;

&lt;p&gt;If you're a developer, startup founder, or small team that wants Stripe data in your own PostgreSQL database — Supabase, Neon, AWS RDS, or any other PostgreSQL instance — this post compares the realistic alternatives so you can pick the right one for your situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Look for Sigma Alternatives
&lt;/h2&gt;

&lt;p&gt;Stripe Sigma was designed for teams that want to query payment data without leaving Stripe. When your actual need is "get my Stripe data into PostgreSQL so I can join it with everything else," you run into friction at every step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiered pricing adds up fast.&lt;/strong&gt; Sigma charges a monthly infrastructure fee plus a per-charge fee, both increasing with volume. At 0-500 charges it's $10/month + $0.02/charge. At 1,001-5,000 charges it jumps to $50/month + $0.016/charge. A growing SaaS with thousands of monthly charges is paying well over $100/month just to write SQL against their own payment data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data stays locked in Stripe.&lt;/strong&gt; You can't join Stripe data with your app's users table, product catalog, or support tickets. Every query exists in isolation. If you want to answer "which customers on the Pro plan have overdue invoices and opened a support ticket this week," Sigma can't help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited visualization.&lt;/strong&gt; Sigma returns tables and basic charts within the Stripe Dashboard. You can schedule queries to receive CSV results via email, but there are no custom dashboards, no embedding in your app, and no way to connect external BI tools. You're limited to what Stripe's UI offers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3-hour data lag.&lt;/strong&gt; Sigma data isn't real-time. New transactions take approximately 3 hours to appear in query results. If you need up-to-the-minute reporting or near-real-time dashboards, Sigma can't deliver that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe-only scope.&lt;/strong&gt; If you also process payments through Paddle, use QuickBooks for accounting, or sync invoices from Xero, you need separate tools for each. Sigma only sees Stripe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL knowledge required.&lt;/strong&gt; Sigma recently added an AI assistant for natural language queries, but the underlying data model is still Stripe's proprietary schema — not your standard &lt;code&gt;customers&lt;/code&gt; and &lt;code&gt;invoices&lt;/code&gt; tables. The learning curve is real even for developers who know SQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sigma solves a real problem — just not the one most developers actually have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alternatives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Automated Sync Tools
&lt;/h3&gt;

&lt;p&gt;Purpose-built sync tools handle the entire pipeline — API calls, pagination, table creation, schema management, and scheduling — so you have Stripe data in your own PostgreSQL database in minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; falls into this category. You connect your PostgreSQL database (Supabase, Neon, AWS RDS, or any PostgreSQL instance), authorize your Stripe account, and it creates the destination tables and syncs the data automatically. No infrastructure to manage, no connectors to configure, and syncs run on a schedule.&lt;/p&gt;

&lt;p&gt;Once your data is in PostgreSQL, you can query it with any SQL tool you already use — psql, DataGrip, Metabase, Retool, or your app's ORM. Join Stripe customers with your own users table. Build dashboards that combine payment data with product analytics. The data is yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fastest setup time (minutes, not hours). Built specifically for the API-to-PostgreSQL use case. Handles table creation, schema management, and incremental syncs. Supports multiple providers beyond Stripe — QuickBooks, Xero, and Paddle — so you can consolidate billing data in one place. &lt;a href="https://codelesssync.com/pricing" rel="noopener noreferrer"&gt;Free tier available&lt;/a&gt; — no credit card required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Less flexible than writing custom code — you get the data types and fields the tool supports rather than arbitrary transformations. Not suitable if you need to transform data during extraction.&lt;/p&gt;

&lt;p&gt;We wrote a &lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;step-by-step guide for syncing Stripe data to PostgreSQL&lt;/a&gt; that covers the full setup process if you want to see what this looks like in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Stripe Data Pipeline
&lt;/h3&gt;

&lt;p&gt;Stripe's own data export product. It syncs your Stripe data to a data warehouse on a schedule — refreshing every 3 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; First-party product from Stripe, so the data mapping is reliable. Fully managed — no infrastructure to host. Includes Sigma, so you get both warehouse sync and in-dashboard queries. Supports Snowflake, Amazon Redshift, Databricks, and cloud storage destinations (S3, Google Cloud Storage, Azure Blob).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; No PostgreSQL support. Data Pipeline exports to Snowflake, Redshift, and Databricks — not to PostgreSQL, Supabase, or Neon. If your stack is PostgreSQL-based, this isn't an option. Pricing is also higher than Sigma alone (subscription fee based on monthly charges, contact Stripe for exact pricing). And it's still Stripe-only data — no QuickBooks, Xero, or Paddle.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Table Dog (tdog)
&lt;/h3&gt;

&lt;p&gt;An open-source CLI tool that downloads your entire Stripe account into a SQL database — SQLite, PostgreSQL, or MySQL.&lt;/p&gt;

&lt;p&gt;On the first run it downloads all Stripe objects. After that, it polls Stripe's &lt;code&gt;/events&lt;/code&gt; endpoint to apply incremental updates. It can run once for a point-in-time snapshot, or as a background daemon for near-real-time sync (less than 1 second behind).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Free and open source. Supports PostgreSQL directly. Near-real-time updates when running as a daemon. Comprehensive — downloads your full Stripe account, not just selected data types. Can also write to SQLite for local analysis without a database server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; CLI-only — no web UI, no dashboard, no visual configuration. You manage hosting, scheduling, and monitoring yourself. Stripe-only — no support for QuickBooks, Xero, Paddle, or other providers. If tdog crashes or the host machine restarts, you need your own process supervision. It's a small open-source project, so check the &lt;a href="https://github.com/tabledog/tdog-cli" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; for current activity before relying on it in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Custom ETL Script
&lt;/h3&gt;

&lt;p&gt;The DIY approach. Write a script using the Stripe SDK that fetches data and inserts it into PostgreSQL. Run it on a schedule with cron, a serverless function, or a task runner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_customers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auto_paging_iter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO stripe_customers (id, email, name, created) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALUES (%s, %s, %s, to_timestamp(%s)) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ON CONFLICT (id) DO UPDATE SET email=EXCLUDED.email, name=EXCLUDED.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;sync_customers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Full control over what data you sync and how it's transformed. Cheap to run — a Lambda function or small cron job costs almost nothing. No vendor dependency. You can customize the schema exactly to your needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; You're building and maintaining everything yourself — pagination logic, rate limit handling, error recovery, schema updates when Stripe changes their API, connection pooling, and monitoring. For one data type (customers), it's manageable. For customers, subscriptions, invoices, charges, refunds, and payment methods — you're maintaining a custom ETL system. Every Stripe API update is a potential maintenance task.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Managed ETL (Airbyte, Fivetran)
&lt;/h3&gt;

&lt;p&gt;Self-hosted (Airbyte) or fully managed (Fivetran) platforms with pre-built Stripe connectors. They handle extraction, loading, and schema management out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Pre-built Stripe connector with hundreds of other data sources available. Handles pagination, rate limiting, schema evolution, and error recovery. Airbyte is open source with a self-hosted option. Fivetran is fully managed with monitoring dashboards and alerting. Both support PostgreSQL as a destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Airbyte requires self-hosting — typically an EC2 instance or Docker setup with a minimum of 2 CPUs and 8GB RAM (4+ CPUs recommended). That's its own infrastructure to monitor, update, and scale. Fivetran's pricing is based on Monthly Active Rows (MAR) — the Free tier covers up to 500K MAR, but the Standard plan charges $2.50 per million MAR plus a $5 base per connector, and a typical small setup (3-5 connectors) runs $300-800/month. If you only need Stripe data in PostgreSQL, both tools are significantly more complex (and expensive) than the problem requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Stripe Sigma&lt;/th&gt;
&lt;th&gt;Codeless Sync&lt;/th&gt;
&lt;th&gt;Data Pipeline&lt;/th&gt;
&lt;th&gt;Table Dog&lt;/th&gt;
&lt;th&gt;Custom Script&lt;/th&gt;
&lt;th&gt;Airbyte&lt;/th&gt;
&lt;th&gt;Fivetran&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQL (Stripe schema)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;CLI commands&lt;/td&gt;
&lt;td&gt;Python / Node&lt;/td&gt;
&lt;td&gt;None (config)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data lives in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stripe Dashboard&lt;/td&gt;
&lt;td&gt;Your PostgreSQL&lt;/td&gt;
&lt;td&gt;Snowflake / Redshift&lt;/td&gt;
&lt;td&gt;Your PostgreSQL&lt;/td&gt;
&lt;td&gt;Your PostgreSQL&lt;/td&gt;
&lt;td&gt;Your PostgreSQL&lt;/td&gt;
&lt;td&gt;Your PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Join with app data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (in warehouse)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Providers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stripe only&lt;/td&gt;
&lt;td&gt;Stripe, QB, Xero, Paddle&lt;/td&gt;
&lt;td&gt;Stripe only&lt;/td&gt;
&lt;td&gt;Stripe only&lt;/td&gt;
&lt;td&gt;Stripe only&lt;/td&gt;
&lt;td&gt;600+ sources&lt;/td&gt;
&lt;td&gt;700+ sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto table creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Incremental sync&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Every 3 hours&lt;/td&gt;
&lt;td&gt;Events-based&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost (low volume)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;From $10/mo (tiered)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://codelesssync.com/pricing" rel="noopener noreferrer"&gt;Free tier&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Contact Stripe&lt;/td&gt;
&lt;td&gt;Free (+ hosting)&lt;/td&gt;
&lt;td&gt;~$1-5/mo&lt;/td&gt;
&lt;td&gt;Free (+ EC2 ~$30/mo)&lt;/td&gt;
&lt;td&gt;Free tier / ~$300+/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick ad-hoc queries&lt;/td&gt;
&lt;td&gt;API-to-PostgreSQL&lt;/td&gt;
&lt;td&gt;Snowflake/Redshift users&lt;/td&gt;
&lt;td&gt;Dev-friendly CLI&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;Many data sources&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When Stripe Sigma Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;This post isn't about Sigma being a bad product — it's about using the right tool for the job. Sigma is the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need a quick answer, not a pipeline.&lt;/strong&gt; "How many refunds did we process last Tuesday?" — if that's the kind of question you're answering, Sigma handles it without any setup. No database to configure, no sync to wait for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your team lives in the Stripe Dashboard.&lt;/strong&gt; If your finance team already spends their day in Stripe and just needs to run occasional queries, Sigma keeps everything in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your charge volume is low.&lt;/strong&gt; At the lowest tier (up to 500 charges), Sigma costs around $20/month. If you're pre-revenue or early-stage, that's cheaper than the time spent setting up an alternative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You only need Stripe data in isolation.&lt;/strong&gt; If you genuinely don't need to join payment data with anything else, Sigma's single-source limitation isn't a limitation at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anything beyond isolated Stripe queries — custom dashboards, cross-source analytics, data ownership, or cost optimization at scale — getting the data into your own PostgreSQL database is the better path.&lt;/p&gt;

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

&lt;p&gt;The best Sigma alternative depends on what you're trying to do. If you want Stripe data in your own PostgreSQL database without building or maintaining a pipeline, a purpose-built sync tool gets you there in minutes. If you need full control over the extraction and transformation, a custom script gives you that flexibility. If you're already running a data platform with dozens of sources, Airbyte or Fivetran makes sense.&lt;/p&gt;

&lt;p&gt;The common thread: once your Stripe data lives in PostgreSQL, you can query it with any tool, join it with any table, and build anything on top of it. That's something Sigma can't offer.&lt;/p&gt;

&lt;p&gt;Give it a try: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How much does Stripe Sigma cost?
&lt;/h3&gt;

&lt;p&gt;Stripe Sigma uses tiered pricing with a monthly infrastructure fee plus a per-charge fee. The lowest tier (0-500 charges) starts at $10/month + $0.02/charge. At 501-1,000 charges it's $25/month + $0.018/charge. At 1,001-5,000 charges it's $50/month + $0.016/charge. Costs scale from there, and 25,000+ charges requires custom pricing. New accounts get a 30-day free trial. If you're processing significant volume, the tiered fees add up quickly — especially when alternatives like &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; offer a free tier that gives you the data in your own database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Stripe Sigma export data to PostgreSQL?
&lt;/h3&gt;

&lt;p&gt;No. Sigma runs queries inside the Stripe Dashboard and returns results there. It has no export-to-database feature. Stripe's separate product, Data Pipeline, can export to Snowflake, Redshift, and Databricks — but not PostgreSQL. To get Stripe data into PostgreSQL (including Supabase, Neon, or AWS RDS), you need a third-party tool or custom script.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the cheapest way to get Stripe data into PostgreSQL?
&lt;/h3&gt;

&lt;p&gt;A custom script using the Stripe SDK and a cron job is the cheapest option at $1-5/month in compute costs. The trade-off is development and maintenance time. If your time is more valuable than a few dollars a month, &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; has a free tier and automates the entire pipeline — setup takes about 5 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Stripe Data Pipeline the same as Stripe Sigma?
&lt;/h3&gt;

&lt;p&gt;No. Sigma is an in-dashboard SQL query tool — you write queries and see results inside Stripe. Data Pipeline is an export product that syncs Stripe data to external warehouses (Snowflake, Redshift, Databricks). Data Pipeline includes Sigma access, but they serve different purposes. Neither exports to PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Stripe Sigma with Supabase or Neon?
&lt;/h3&gt;

&lt;p&gt;Not directly. Sigma only works inside the Stripe Dashboard — there's no way to connect it to an external database. To get Stripe data into Supabase or Neon, you need a sync tool that connects to your database and pulls data from the Stripe API. &lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-neon-postgresql" rel="noopener noreferrer"&gt;Codeless Sync supports both Supabase and Neon&lt;/a&gt; as destinations with a one-click setup.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/stripe-webhooks-vs-database-sync" rel="noopener noreferrer"&gt;Stripe Webhooks vs Database Sync: Which is Better?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/5-ways-to-get-stripe-data-into-postgresql" rel="noopener noreferrer"&gt;5 Ways to Get Stripe Data into PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/aws-glue-alternatives-simpler-ways-to-sync-api-data-to-rds" rel="noopener noreferrer"&gt;AWS Glue Alternatives: Simpler Ways to Sync API Data to RDS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;Connect Your Database to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>stripe</category>
      <category>database</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AWS Glue Alternatives: Simpler Ways to Sync API Data to RDS</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Thu, 19 Mar 2026 18:56:23 +0000</pubDate>
      <link>https://forem.com/ilshadyx/aws-glue-alternatives-simpler-ways-to-sync-api-data-to-rds-3n09</link>
      <guid>https://forem.com/ilshadyx/aws-glue-alternatives-simpler-ways-to-sync-api-data-to-rds-3n09</guid>
      <description>&lt;p&gt;&lt;em&gt;AWS Glue is powerful but overkill for syncing API data to RDS PostgreSQL. Here are simpler alternatives — from Lambda scripts to fully automated sync tools — compared side by side.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 19 Mar 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;AWS Glue can transform terabytes of data across S3 buckets, orchestrate complex ETL workflows, and handle schema evolution at scale. It's also one of the most over-engineered ways to get your Stripe customers into a Postgres table.&lt;/p&gt;

&lt;p&gt;If you've ever spent an afternoon configuring a Glue connection to your VPC, writing a PySpark script for what should be a simple API call, or debugging a crawler that keeps inferring the wrong schema — you're not alone. "AWS Glue alternative" is one of the most searched terms in the AWS data tooling space, and the reason is simple: most developers don't need what Glue offers.&lt;/p&gt;

&lt;p&gt;If you're a solo developer, startup founder, or small team that just needs billing data from Stripe, QuickBooks, Xero, or Paddle in your RDS PostgreSQL database — this post compares the realistic alternatives, from custom scripts to fully managed tools, so you can pick the right one for your situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Look for Glue Alternatives
&lt;/h2&gt;

&lt;p&gt;AWS Glue was designed for data engineering teams processing large volumes of data across AWS services — S3 to Redshift, DynamoDB to S3, that kind of thing. When your actual need is "call an API and put the JSON into a Postgres table," you run into friction at every step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold start and runtime costs.&lt;/strong&gt; Glue Spark ETL jobs default to 10 DPUs (minimum 2), at $0.44 per DPU-hour. Even the lightest Python Shell job (0.0625 DPU) still carries a 1-minute minimum per run. For a job that takes 10 seconds to fetch 500 records from Stripe, you're paying for far more infrastructure than the task requires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PySpark for simple tasks.&lt;/strong&gt; Glue's default runtime is Apache Spark. Writing PySpark to paginate a REST API and insert rows into RDS is like using a forklift to move a chair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python Shell jobs aren't much better.&lt;/strong&gt; Glue does offer a lighter Python Shell option, but you still need to manage VPC connections, IAM roles, Secrets Manager references, and packaging any dependencies your script needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth is your problem.&lt;/strong&gt; If your data source uses OAuth (QuickBooks, Xero), you need to handle token storage, refresh logic, and error handling yourself. One expired token and your pipeline silently stops producing data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectors don't remove the complexity.&lt;/strong&gt; Glue now has native SaaS connectors and a REST API connector, but using them still means configuring Glue jobs, IAM roles, and VPC connections. The connector handles the HTTP call — everything else is still on you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Glue solves a real problem — just not this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alternatives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Custom Lambda Function
&lt;/h3&gt;

&lt;p&gt;The DIY baseline. Write a Lambda function that calls your API, transforms the response, and inserts into RDS. Trigger it on a schedule with EventBridge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Fetch from API
&lt;/span&gt;    &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PoolManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://api.stripe.com/v1/customers?limit=100&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer sk_live_...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Insert into RDS
&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO stripe_customers (id, email, name) VALUES (%s, %s, %s) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ON CONFLICT (id) DO UPDATE SET email=EXCLUDED.email, name=EXCLUDED.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;c&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;c&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Cheap to run, flexible, no Spark overhead, you control everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; You're writing and maintaining all the code — pagination, rate limiting, error handling, connection pooling (RDS Proxy solves this but it's another service to configure and pay for), schema updates when the API changes, and OAuth token management for providers that require it. For one data type from one provider, it's manageable. For multiple providers and data types, you're building and maintaining a custom ETL system.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Automated Sync Tools
&lt;/h3&gt;

&lt;p&gt;Purpose-built sync tools handle the entire pipeline — API calls, pagination, table creation, schema management, and scheduling — so you're up and running in minutes instead of days.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; falls into this category. You connect your RDS instance, authorize your billing provider (Stripe, QuickBooks, Xero, or Paddle), and it creates the destination table and syncs the data automatically. There's no infrastructure to manage, no connectors to configure, and syncs run automatically on a schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fastest setup time (minutes, not hours). Built specifically for the API-to-PostgreSQL use case. Handles table creation, schema management, OAuth token refresh, and incremental syncs. No infrastructure to host or maintain. &lt;a href="https://codelesssync.com/pricing" rel="noopener noreferrer"&gt;Free tier available&lt;/a&gt; — no credit card required to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Less flexible than writing custom code — you get the data types and fields the tool supports rather than arbitrary transformations. Not suitable if you need to transform data during extraction or sync from non-supported APIs.&lt;/p&gt;

&lt;p&gt;We wrote a &lt;a href="https://codelesssync.com/blog/how-to-sync-billing-data-to-aws-rds-postgresql" rel="noopener noreferrer"&gt;step-by-step walkthrough for syncing billing data to AWS RDS&lt;/a&gt; that covers the full setup process if you want to see what this looks like in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a solo developer or small team&lt;/strong&gt; syncing one or two billing providers, this is the fastest path from "I need this data in RDS" to actually querying it. The remaining alternatives below offer more flexibility or scale — but each requires you to build or manage something.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Open-Source ETL (Airbyte, Meltano)
&lt;/h3&gt;

&lt;p&gt;Self-hosted tools like Airbyte and Meltano come with pre-built connectors for Stripe, QuickBooks, and other SaaS APIs. They handle pagination, rate limiting, and schema management out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Pre-built API connectors, open source, handles the hard parts of API extraction, community-maintained. Supports 600+ data sources if you need more than billing providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; You need to host and maintain the tool itself — typically an EC2 instance or ECS cluster running the Airbyte server, scheduler, and worker containers. That's its own infrastructure to monitor, update, and scale. Airbyte's resource requirements aren't trivial either: the recommended spec is 4 CPUs and 8GB RAM for the server alone. For a team that wanted to avoid infrastructure overhead, this trades one kind for another.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Managed ETL (Fivetran, Stitch)
&lt;/h3&gt;

&lt;p&gt;Fully managed SaaS platforms that sync data from APIs to databases. No infrastructure to maintain — they handle connectors, scheduling, and error recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Truly hands-off. Reliable connectors, automatic schema handling, monitoring dashboards, alerting. The most mature option if you need dozens of data sources across an organization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Pricing. Fivetran charges based on Monthly Active Rows (MAR) — rows that are created or updated in a billing period. They offer a Free tier (up to 500K MAR) and a Starter tier for smaller teams, but the Standard plan starts at roughly $1,200/month base — firmly enterprise territory. Stitch (now part of Qlik, via the Talend acquisition — though Qlik is directing new users toward Qlik Talend Cloud) has similar volume-based pricing. If you only need one or two data sources synced to RDS, these tools are overkill for the job.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Step Functions + Lambda
&lt;/h3&gt;

&lt;p&gt;If you need orchestration — retries, parallel execution across providers, conditional logic between multiple data types — Step Functions can coordinate a workflow of Lambda functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Proper retry logic, error handling, and parallelism without custom orchestration code. Good fit if you already have Lambda functions and need to chain them together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; The most complex alternative on this list. You're maintaining state machine definitions, multiple Lambda functions, IAM roles for each, and the Step Functions execution costs on top. The individual Lambda functions still have all the same problems listed in option 1 — Step Functions just coordinate them. Only worth it if your pipeline genuinely needs orchestration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;AWS Glue&lt;/th&gt;
&lt;th&gt;Lambda&lt;/th&gt;
&lt;th&gt;Codeless Sync&lt;/th&gt;
&lt;th&gt;Airbyte (Self-Hosted)&lt;/th&gt;
&lt;th&gt;Fivetran&lt;/th&gt;
&lt;th&gt;Step Functions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;30 min&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PySpark / Python&lt;/td&gt;
&lt;td&gt;Python / Node&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None (config)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Python / Node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed (AWS)&lt;/td&gt;
&lt;td&gt;Managed (AWS)&lt;/td&gt;
&lt;td&gt;Fully managed&lt;/td&gt;
&lt;td&gt;Self-hosted (EC2/ECS)&lt;/td&gt;
&lt;td&gt;Fully managed&lt;/td&gt;
&lt;td&gt;Managed (AWS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API connectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;REST + SaaS (limited)&lt;/td&gt;
&lt;td&gt;None (custom)&lt;/td&gt;
&lt;td&gt;Stripe, QB, Xero, Paddle&lt;/td&gt;
&lt;td&gt;600+ pre-built&lt;/td&gt;
&lt;td&gt;700+ pre-built&lt;/td&gt;
&lt;td&gt;None (custom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OAuth handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Table creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Incremental sync&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost (low volume)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$15-30/mo&lt;/td&gt;
&lt;td&gt;~$1-5/mo&lt;/td&gt;
&lt;td&gt;&lt;a href="https://codelesssync.com/pricing" rel="noopener noreferrer"&gt;Free tier available&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free (+ EC2 ~$30/mo)&lt;/td&gt;
&lt;td&gt;Free tier / $1,200+/mo&lt;/td&gt;
&lt;td&gt;~$5-15/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Large-scale ETL&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;API-to-PostgreSQL&lt;/td&gt;
&lt;td&gt;Many data sources&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Complex orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When AWS Glue Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;This post isn't about Glue being a bad tool — it's about using the right tool for the job. Glue is the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You're moving data between AWS services at scale&lt;/strong&gt; — S3 to Redshift, DynamoDB exports, cross-account data sharing. This is what Glue was designed for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need complex transformations&lt;/strong&gt; — deduplication, joining multiple sources during extraction, applying business logic before loading. Spark's processing model handles this well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your data volumes are genuinely large&lt;/strong&gt; — millions of records per run, where Spark's distributed processing actually provides a performance benefit over single-threaded scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already have a data engineering team&lt;/strong&gt; — if you have dedicated engineers who know Spark and manage Glue jobs daily, the operational overhead isn't incremental.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For syncing a few thousand records from a billing API to RDS on a schedule, Glue is the wrong abstraction. You don't need distributed computing for a task that a single HTTP request and a database INSERT can handle.&lt;/p&gt;

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

&lt;p&gt;The best Glue alternative depends on what you actually need. If you want full control and don't mind maintaining code, a Lambda function is the cheapest path. If you're syncing dozens of data sources across an organization, Fivetran or Airbyte earns its cost. If you need billing data in your RDS database without the overhead of building or managing a pipeline, a focused sync tool gets you there in minutes.&lt;/p&gt;

&lt;p&gt;The common thread across all these alternatives: none of them require you to learn PySpark, configure Glue connections, or debug crawler schemas for what is fundamentally a simple data movement task.&lt;/p&gt;

&lt;p&gt;Give it a try: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is AWS Glue free?
&lt;/h3&gt;

&lt;p&gt;No. Glue charges $0.44 per DPU-hour with a 1-minute minimum per job run. Spark ETL jobs default to 10 DPUs (minimum 2), while Python Shell jobs start at 0.0625 DPU. There's a small free tier for the Glue Data Catalog, but the ETL jobs themselves always cost. For a lightweight API sync that runs daily, expect $15-30/month — mostly wasted on idle compute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can AWS Glue connect to REST APIs?
&lt;/h3&gt;

&lt;p&gt;Glue now has native SaaS connectors and a REST API connector, so it can make API calls natively. But using them still means configuring Glue jobs, IAM roles, VPC connections, and dealing with Glue's operational complexity. For SaaS APIs like Stripe, QuickBooks, or Xero, you're still handling pagination logic, authentication, and error handling within the Glue framework — the connector doesn't remove the overhead, it just handles the HTTP layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the cheapest way to sync API data to RDS PostgreSQL?
&lt;/h3&gt;

&lt;p&gt;A custom Lambda function triggered by EventBridge is the cheapest option at $1-5/month for low volumes. The trade-off is development and maintenance time — you're writing and maintaining the sync code yourself. If your time is more valuable than a few dollars a month, &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; has a free tier and automates the entire pipeline — setup takes minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need AWS Glue for a simple ETL pipeline?
&lt;/h3&gt;

&lt;p&gt;For most API-to-database syncs, no. Glue was designed for large-scale data processing across AWS services — not for calling a REST API and inserting rows into Postgres. If your pipeline is "fetch JSON from an API, put it in a table," every alternative in this post is simpler and cheaper than Glue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Airbyte with AWS RDS?
&lt;/h3&gt;

&lt;p&gt;Yes. Airbyte supports PostgreSQL as a destination, including RDS instances. The main consideration is hosting — you'll need to run the Airbyte server on an EC2 instance or ECS cluster within your VPC. If you're already running other self-hosted tools on AWS, adding Airbyte is straightforward. If not, you're taking on new infrastructure to manage.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-billing-data-to-aws-rds-postgresql" rel="noopener noreferrer"&gt;How to Sync Your Billing Data to AWS RDS PostgreSQL (Without Building a Pipeline)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/stripe-webhooks-vs-database-sync" rel="noopener noreferrer"&gt;Stripe Webhooks vs Database Sync: Which is Better?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-quickbooks-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync QuickBooks Data to PostgreSQL Automatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;Connect Your Database to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>postgres</category>
      <category>aws</category>
      <category>database</category>
    </item>
    <item>
      <title>How to Sync Your Billing Data to AWS RDS PostgreSQL (Without Building a Pipeline)</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Wed, 11 Mar 2026 19:24:45 +0000</pubDate>
      <link>https://forem.com/ilshadyx/how-to-sync-your-billing-data-to-aws-rds-postgresql-without-building-a-pipeline-55g8</link>
      <guid>https://forem.com/ilshadyx/how-to-sync-your-billing-data-to-aws-rds-postgresql-without-building-a-pipeline-55g8</guid>
      <description>&lt;p&gt;&lt;em&gt;Skip AWS Glue, Lambda functions, and Step Functions. Sync Stripe, QuickBooks, Xero, or Paddle data into RDS PostgreSQL in minutes — no infrastructure to maintain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 11 Mar 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;AWS gives you a dozen ways to move data between services. The problem is that most of them were designed for data engineering teams running complex pipelines at scale — not for a developer who just wants their billing data in a Postgres table.&lt;/p&gt;

&lt;p&gt;Whether you're pulling customer records from Stripe, invoices from QuickBooks, contacts from Xero, or subscriptions from Paddle, the story is the same: you need that data in your RDS database, and you don't want to spend a week building the pipeline to get it there.&lt;/p&gt;

&lt;p&gt;This guide shows a faster path. Connect your RDS PostgreSQL instance, authorize your billing provider, and your data appears as a queryable table. No Lambda functions, no Glue jobs, no CloudWatch alarms to monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AWS Pipeline Trap
&lt;/h2&gt;

&lt;p&gt;When you're already inside the AWS ecosystem, it's natural to reach for AWS-native tools. Need data moved? There's a service for that. Probably three. Here's what building a billing-data-to-RDS pipeline typically looks like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Glue.&lt;/strong&gt; You'd create a custom Python shell job that calls your billing provider's API, transforms the JSON response, and writes to RDS. But first you need a Glue connection to your VPC, an IAM role with the right permissions, and a Secrets Manager entry for your API credentials. And that's just for one provider — add QuickBooks or Xero and you're maintaining separate jobs, each with their own OAuth token refresh logic. For a table of customer records, this is like hiring a moving company to carry a suitcase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lambda + EventBridge.&lt;/strong&gt; A scheduled Lambda function that pulls from an API and inserts into RDS. Simpler than Glue, but you're still writing pagination logic, handling rate limits, managing database connections (Lambda's ephemeral nature makes connection pooling awkward), and packaging dependencies. For OAuth-based providers like QuickBooks and Xero, you also need to handle token storage and refresh — one missed refresh and your pipeline silently stops. When something breaks at 2am, you're debugging CloudWatch logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step Functions.&lt;/strong&gt; If you want orchestration — retry logic, error handling, multiple data types across multiple providers — you might reach for Step Functions to coordinate your Lambdas. Now you're maintaining a state machine definition, multiple Lambda functions, IAM roles for each, and the glue code that ties them together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Data Pipeline.&lt;/strong&gt; AWS deprecated this service — it no longer accepts new customers. AWS themselves recommend Glue or Step Functions instead.&lt;/p&gt;

&lt;p&gt;Every one of these approaches works. But they all share the same problem: you're building infrastructure to solve what is fundamentally a plumbing task. The initial setup takes a day or two per provider. The maintenance — fixing broken connections, refreshing OAuth tokens, updating schemas when APIs change, handling edge cases in pagination — goes on indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RDS PostgreSQL for Billing Data?
&lt;/h2&gt;

&lt;p&gt;If your application already runs on RDS, keeping your billing data in the same database instance (or at least the same VPC) gives you advantages that no external analytics tool can match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-latency JOINs&lt;/strong&gt; — your &lt;code&gt;users&lt;/code&gt; table and your billing tables live on the same database engine. Joining Stripe customers to your own users, or QuickBooks invoices to your internal orders, is a standard query — not an API orchestration problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing backups cover everything&lt;/strong&gt; — RDS automated snapshots already protect your application data. Billing data synced into the same instance is included automatically. No separate backup strategy needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC security&lt;/strong&gt; — your billing data ends up inside your private network rather than living in a third-party warehouse. For teams in regulated industries, this matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use your existing tools&lt;/strong&gt; — whatever you already use to query RDS (pgAdmin, DBeaver, Metabase, your application's ORM) works for billing data too. No new dashboards to learn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read Replicas for free analytics&lt;/strong&gt; — if you have an RDS Read Replica for reporting, your synced billing data is automatically available there too. Run heavy analytical queries without touching your primary instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole point of a managed database is to reduce operational overhead. Building a custom data pipeline on top of it adds the overhead back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing Your RDS Instance
&lt;/h2&gt;

&lt;p&gt;Before connecting any external service to RDS, there are a few AWS-specific steps to handle. This is where RDS differs significantly from platforms like Supabase or Neon, which are accessible by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Group Configuration
&lt;/h3&gt;

&lt;p&gt;Your RDS instance lives inside a VPC and is protected by a security group. By default, it only accepts connections from resources within the same VPC — which means an external service like Codeless Sync can't reach it.&lt;/p&gt;

&lt;p&gt;You need to add an inbound rule to your RDS security group:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://console.aws.amazon.com/rds/" rel="noopener noreferrer"&gt;Amazon RDS Console&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Select your database instance and click on the &lt;strong&gt;VPC security group&lt;/strong&gt; link&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Inbound rules&lt;/strong&gt; → &lt;strong&gt;Edit inbound rules&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a rule:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; 5432&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;code&gt;0.0.0.0/0&lt;/code&gt; (or restrict to specific IPs if your security policy requires it)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the rule&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; If your RDS instance has &lt;strong&gt;Public accessibility&lt;/strong&gt; set to "No", you'll need to change it to "Yes" for external connections. You can do this from the RDS Console → &lt;strong&gt;Modify&lt;/strong&gt; → &lt;strong&gt;Connectivity&lt;/strong&gt; → &lt;strong&gt;Public access&lt;/strong&gt;. This assigns a public DNS endpoint to your instance while the security group still controls who can actually connect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Public accessibility also requires your RDS instance to be in a &lt;strong&gt;public subnet&lt;/strong&gt; — one with a route to an Internet Gateway. If your instance is in a private subnet, you'll need to either move it to a public subnet or set up a NAT Gateway / bastion host for external access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grab Your Connection String
&lt;/h3&gt;

&lt;p&gt;Your RDS connection string follows this format:&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="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amazonaws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dbname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the endpoint in the RDS Console under your instance's &lt;strong&gt;Connectivity &amp;amp; security&lt;/strong&gt; tab. The endpoint looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-instance.abc123xyz.eu-west-2.rds.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine it with your master username, password, port (default 5432), and database name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSL note:&lt;/strong&gt; RDS instances have SSL enabled by default. Most PostgreSQL clients (and Codeless Sync) handle this automatically. If you run into SSL connection issues, append &lt;code&gt;?sslmode=require&lt;/code&gt; to your connection string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your First Sync
&lt;/h2&gt;

&lt;p&gt;With your RDS instance accessible, here's the setup using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt;. We'll use Stripe as the example, but the process is the same for QuickBooks, Xero, and Paddle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Pick Your Provider and Data Type
&lt;/h3&gt;

&lt;p&gt;In the Codeless Sync dashboard, click &lt;strong&gt;Create Sync Configuration&lt;/strong&gt; to open the wizard. Select your billing provider — &lt;strong&gt;Stripe&lt;/strong&gt;, &lt;strong&gt;QuickBooks&lt;/strong&gt;, &lt;strong&gt;Xero&lt;/strong&gt;, or &lt;strong&gt;Paddle&lt;/strong&gt; — then choose a data type. Each provider offers multiple options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — Customers, Invoices, Subscriptions, Payment Intents, Products, Prices, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QuickBooks&lt;/strong&gt; — Customers, Invoices, Payments, Items, Accounts, Vendors, Bills, Estimates, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Xero&lt;/strong&gt; — Contacts, Invoices, Payments, Accounts, Bank Transactions, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paddle&lt;/strong&gt; — Customers, Subscriptions, Transactions, Products, Prices, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with Customers for any provider — it's the easiest to verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Connect Your RDS Database
&lt;/h3&gt;

&lt;p&gt;Select or create a database project. Choose &lt;strong&gt;AWS RDS&lt;/strong&gt; as the platform and paste your connection string from the previous section. The connection is tested automatically.&lt;/p&gt;

&lt;p&gt;If the test fails, double-check your security group rules and public accessibility setting — those are the most common blockers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Authorize Your Provider
&lt;/h3&gt;

&lt;p&gt;How this step works depends on the provider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — enter a restricted API key (&lt;code&gt;rk_test_&lt;/code&gt; or &lt;code&gt;rk_live_&lt;/code&gt;) with read-only permissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QuickBooks / Xero&lt;/strong&gt; — click &lt;strong&gt;Connect&lt;/strong&gt; and authorize through the provider's OAuth consent screen. Codeless Sync handles token storage and automatic refresh — no OAuth plumbing on your end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paddle&lt;/strong&gt; — enter your API key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Test Connection&lt;/strong&gt; to validate before continuing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create the Table
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Auto-Create Table&lt;/strong&gt; and the destination table is created in your RDS database with the correct schema — columns matched to your provider's fields, proper data types, and indexes for common queries.&lt;/p&gt;

&lt;p&gt;If you prefer to review the SQL first, copy the template and run it in your preferred client (pgAdmin, DBeaver, or the RDS Query Editor if you have it enabled).&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Verify Table&lt;/strong&gt; to confirm the structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Sync and Verify
&lt;/h3&gt;

&lt;p&gt;Name your configuration, click &lt;strong&gt;Create&lt;/strong&gt;, then hit &lt;strong&gt;Sync Now&lt;/strong&gt;. The first sync pulls all records — typically seconds to a couple of minutes depending on volume.&lt;/p&gt;

&lt;p&gt;Once complete, connect to your RDS instance and check your data:&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="c1"&gt;-- If you synced Stripe customers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- If you synced QuickBooks customers&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_customers&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your billing data is now sitting in RDS, queryable like any other table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queries That Make This Worth It
&lt;/h2&gt;

&lt;p&gt;Billing data in Postgres is only valuable if you use it. Here's what becomes possible once your provider tables live alongside your application data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link billing customers to your application users — without any API calls:&lt;/strong&gt;&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="c1"&gt;-- Works with Stripe&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stripe_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_since&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Works with QuickBooks&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;quickbooks_customers&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the query that justifies the entire setup. Joining your internal user records with billing data requires exactly zero API calls — both tables live on the same RDS instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly revenue trend from paid invoices:&lt;/strong&gt;&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="c1"&gt;-- Stripe invoices (amounts in cents)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;invoices_paid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- QuickBooks invoices (amounts in dollars)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;invoices_paid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Identify customers with expiring Stripe subscriptions:&lt;/strong&gt;&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_period_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_period_end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;time_remaining&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_subscriptions&lt;/span&gt; &lt;span class="n"&gt;ss&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_period_end&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_period_end&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catching renewals before they lapse — the kind of proactive query that's impractical with API polling but trivial with SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outstanding QuickBooks invoices — who owes you money:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;doc_number&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;days_overdue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these run against your RDS instance. Same connection, same tools, same permissions your team already uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping Data Fresh
&lt;/h2&gt;

&lt;p&gt;A one-time sync is useful for exploration. For production use, set up a scheduled sync — hourly, daily, or a custom interval — and the data stays current without any manual intervention.&lt;/p&gt;

&lt;p&gt;After the initial full sync, subsequent runs are incremental: only records changed since the last run are fetched. This keeps sync times short, API usage low, and your RDS instance under minimal additional load.&lt;/p&gt;

&lt;p&gt;Need multiple providers? Create a separate sync configuration for each. They run independently — your Stripe customers can sync hourly while your QuickBooks invoices sync daily. Each provider's data lands in its own table, ready to JOIN with everything else in your database.&lt;/p&gt;

&lt;p&gt;No Lambda functions to monitor. No CloudWatch alarms to configure. No IAM roles to maintain. No OAuth tokens to refresh. The syncs run, your tables update, and you move on to the work that actually matters.&lt;/p&gt;

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

&lt;p&gt;AWS RDS is built for reliability and performance. Building custom data pipelines on top of it — with Glue jobs, Lambda functions, and Step Functions — adds the operational complexity that RDS was supposed to eliminate.&lt;/p&gt;

&lt;p&gt;If your app runs on RDS and you use Stripe, QuickBooks, Xero, or Paddle for billing, the data belongs in the same database. Connect your instance, authorize your provider, and your billing data is just another table you can query.&lt;/p&gt;

&lt;p&gt;Give it a try: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/stripe-to-aws-rds" rel="noopener noreferrer"&gt;Sync Stripe Data to AWS RDS — No Code, Auto-Create Tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-quickbooks-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync QuickBooks Data to PostgreSQL Automatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;Connect Your Database to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>How to Sync QuickBooks Data to PostgreSQL Automatically</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Sun, 08 Mar 2026 10:46:04 +0000</pubDate>
      <link>https://forem.com/ilshadyx/how-to-sync-quickbooks-data-to-postgresql-automatically-45pb</link>
      <guid>https://forem.com/ilshadyx/how-to-sync-quickbooks-data-to-postgresql-automatically-45pb</guid>
      <description>&lt;p&gt;&lt;em&gt;Automate QuickBooks to PostgreSQL sync — skip the OAuth plumbing, incremental polling, and pipeline maintenance. Set it up once, keep your data fresh forever.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 8 Mar 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;QuickBooks is where your accounting lives. PostgreSQL is where your application data lives. Getting the two into the same database shouldn't require weeks of OAuth plumbing and polling logic — but that's exactly what most developers end up building.&lt;/p&gt;

&lt;p&gt;Sure, you could prompt an AI to scaffold the integration for you. You'd get a working prototype in an afternoon. But then you're still on the hook for token refresh logic, incremental polling schedules, error recovery, schema updates, and the ongoing maintenance that comes with any data pipeline. The code isn't the hard part anymore — keeping it running is.&lt;/p&gt;

&lt;p&gt;This guide walks through setting up automated QuickBooks-to-PostgreSQL sync in about five minutes. You connect your database, authorize QuickBooks with one click, and your customers, invoices, or payments appear as a regular Postgres table — and stay up to date without you touching it again.&lt;/p&gt;

&lt;p&gt;But first, it's worth understanding why this is harder than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the QuickBooks API Is Painful
&lt;/h2&gt;

&lt;p&gt;If you've worked with Stripe's API, QuickBooks will feel like a different world. The complexity starts before you make your first request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0 is mandatory.&lt;/strong&gt; There's no simple API key. Every QuickBooks integration requires a full OAuth 2.0 flow — registering an app in the Intuit Developer portal, handling authorization redirects, storing tokens, and refreshing them before they expire. Miss a token refresh and your integration silently stops working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No webhooks for data sync.&lt;/strong&gt; QuickBooks supports webhooks, but only as notifications that something changed — they don't include the actual data. You still need to call the API to fetch the updated records. Most teams end up building an incremental polling system that queries QuickBooks periodically for changes. That means managing timestamps, handling pagination, and dealing with QuickBooks' 500-request-per-minute rate limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accounting data is complex.&lt;/strong&gt; QuickBooks entities have deep relationships. An invoice references a customer, line items reference products, payments reference invoices. Getting a complete picture means querying multiple endpoints and joining the results yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema differences.&lt;/strong&gt; QuickBooks field names and structures don't map cleanly to a relational database. Amounts can be nested, dates come in different formats, and custom fields add another layer of complexity.&lt;/p&gt;

&lt;p&gt;Most teams that need QuickBooks data in PostgreSQL end up in one of three places:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom integration.&lt;/strong&gt; Build an OAuth flow, write polling logic, design table schemas, handle token refresh, manage error recovery. AI can generate the boilerplate fast, but you're still maintaining it — fixing edge cases when tokens expire at 3am, handling schema changes when QuickBooks updates their API, debugging silent failures in your polling logic. The initial build isn't the problem. The ongoing maintenance is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise ETL tools.&lt;/strong&gt; Platforms like Fivetran or Airbyte support QuickBooks connectors. But they're designed for data teams running warehouses like Snowflake or BigQuery. If you just want a table in your Postgres database, you're paying enterprise prices for a fraction of the capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSV exports.&lt;/strong&gt; QuickBooks lets you export reports as CSV files. Fine for a one-time analysis, useless for keeping data current in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL for Accounting Data?
&lt;/h2&gt;

&lt;p&gt;Having your QuickBooks data in PostgreSQL alongside your application data unlocks things that are impossible through the API alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JOIN accounting with app data&lt;/strong&gt; — match QuickBooks customers to your &lt;code&gt;users&lt;/code&gt; table, link invoices to your internal orders, or cross-reference payments with your billing records. One query instead of multiple API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query without rate limits&lt;/strong&gt; — once the data is in Postgres, run the same query a thousand times without worrying about QuickBooks' 500 req/min throttle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real SQL for accounting reports&lt;/strong&gt; — revenue by month, outstanding invoices, customer balances, aging reports. All standard SQL instead of navigating QuickBooks' limited reporting API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical analysis&lt;/strong&gt; — QuickBooks' API is optimized for recent data. With synced tables, you can analyze trends across months or years with simple &lt;code&gt;GROUP BY&lt;/code&gt; queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works with any PostgreSQL host&lt;/strong&gt; — whether you're on Supabase, Neon, Railway, AWS RDS, or self-hosted Postgres, the data lives where your app already runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five Steps to Automated Sync
&lt;/h2&gt;

&lt;p&gt;Here's the setup using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt;, which handles the OAuth flow, incremental syncing, token refresh, and schema mapping — so you set it up once and it just runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Connect Your Database
&lt;/h3&gt;

&lt;p&gt;In Codeless Sync, start by adding your PostgreSQL database. Paste your connection string — it works with any PostgreSQL host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://user:pass@your-host.example.com/dbname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection is tested automatically to make sure everything works before you proceed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick Your Data Type
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Create Sync Configuration&lt;/strong&gt; to open the wizard. Select &lt;strong&gt;QuickBooks&lt;/strong&gt; as the provider and choose a data type. Available types include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customers&lt;/strong&gt; — contact details, balances, payment terms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invoices&lt;/strong&gt; — line items, amounts, due dates, payment status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt; — received payments linked to invoices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Items&lt;/strong&gt; — products and services in your catalog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounts&lt;/strong&gt; — your chart of accounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendors&lt;/strong&gt; — supplier information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bills&lt;/strong&gt; — money you owe to vendors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimates&lt;/strong&gt; — quotes sent to customers&lt;/li&gt;
&lt;li&gt;And more (purchases, deposits, credit memos, sales receipts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Customers is a good starting point — it's easy to verify and gives you a feel for how the sync works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Authorize QuickBooks
&lt;/h3&gt;

&lt;p&gt;Instead of building and maintaining an OAuth 2.0 flow, you click &lt;strong&gt;Connect to QuickBooks&lt;/strong&gt; and authorize access through Intuit's standard consent screen. One click, and the connection is established.&lt;/p&gt;

&lt;p&gt;Codeless Sync handles the developer app registration, redirect URIs, token storage, and automatic token refresh behind the scenes — that's one less thing silently breaking in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create the Table
&lt;/h3&gt;

&lt;p&gt;Codeless Sync needs a destination table in your database. Click &lt;strong&gt;Auto-Create Table&lt;/strong&gt; and the correct schema is created automatically — columns matched to QuickBooks fields, proper data types, and indexes for common queries.&lt;/p&gt;

&lt;p&gt;If you'd rather review the SQL first, copy the template and run it in your database client.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Verify Table&lt;/strong&gt; to confirm the structure is correct before proceeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Sync and Verify
&lt;/h3&gt;

&lt;p&gt;Name your configuration, click &lt;strong&gt;Create&lt;/strong&gt;, then hit &lt;strong&gt;Sync Now&lt;/strong&gt; from the dashboard. The first sync pulls all matching records from QuickBooks. Depending on your data volume, this typically takes a few seconds to a couple of minutes.&lt;/p&gt;

&lt;p&gt;Once complete, open your database client and check:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_customers&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see your QuickBooks customers, you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queries That Make This Worth It
&lt;/h2&gt;

&lt;p&gt;Accounting data in Postgres is only valuable if you use it. Here are queries that show what becomes possible once your QuickBooks tables live alongside your application data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue from paid invoices by month:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Outstanding invoices — who owes you money:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;doc_number&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;due_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;days_overdue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of query that's painful through the QuickBooks API — it requires paginating through all invoices, filtering client-side, and calculating overdue days yourself. In Postgres, it's five lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Match QuickBooks customers to your application users:&lt;/strong&gt;&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;quickbooks_customers&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;qc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Joining data from two different systems — your app and your accounting platform — in a single query. This is what makes syncing to PostgreSQL worthwhile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer balance summary:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'paid up'&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'low balance'&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'outstanding'&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;quickbooks_customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these run directly against your PostgreSQL database. No API calls, no rate limits, no waiting for data pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set It and Forget It
&lt;/h2&gt;

&lt;p&gt;The real value isn't the initial sync — it's never thinking about it again. Set up scheduled syncs (hourly, daily, or a custom interval) and your tables stay current automatically.&lt;/p&gt;

&lt;p&gt;After the initial full sync, subsequent runs are incremental: only records that changed since the last run are fetched. This keeps sync times short and stays well within QuickBooks' rate limits.&lt;/p&gt;

&lt;p&gt;OAuth tokens are refreshed automatically. Schema mappings are maintained for you. If a sync fails, you get notified — no silent data staleness. It's the kind of pipeline that would take weeks to build reliably, running in the background without you maintaining it.&lt;/p&gt;

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

&lt;p&gt;If your application uses PostgreSQL and your accounting runs on QuickBooks, the data from both systems belongs in the same database. You could build the pipeline yourself — AI makes the initial code easy enough — but then you're maintaining OAuth tokens, polling logic, error recovery, and schema updates indefinitely. That's not a build problem, it's a maintenance burden.&lt;/p&gt;

&lt;p&gt;Set it up in five minutes, schedule your sync, and move on to the work that actually matters. Your accounting data stays current as just another table you can query.&lt;/p&gt;

&lt;p&gt;Give it a try: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/getting-started/quick-start" rel="noopener noreferrer"&gt;Connect Your Database to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/sql-templates/quickbooks" rel="noopener noreferrer"&gt;QuickBooks SQL Templates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>quickbooks</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Easiest Way to Sync Stripe Data to Neon Postgres</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Tue, 03 Mar 2026 12:08:49 +0000</pubDate>
      <link>https://forem.com/ilshadyx/the-easiest-way-to-sync-stripe-data-to-neon-postgres-4ji9</link>
      <guid>https://forem.com/ilshadyx/the-easiest-way-to-sync-stripe-data-to-neon-postgres-4ji9</guid>
      <description>&lt;p&gt;&lt;em&gt;Put your Stripe customers, invoices, and subscriptions into Neon's serverless Postgres — no code, no webhooks, no custom pipelines. A step-by-step walkthrough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 3 Mar 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Getting Stripe data into Neon takes about five minutes and zero lines of code. You connect your database, paste a Stripe key, and your customers, invoices, or subscriptions appear as a regular Postgres table — ready to query right next to your application data.&lt;/p&gt;

&lt;p&gt;But before we get into the how, it's worth understanding the problem — because it explains why so many developers end up looking for a shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem with Stripe Data
&lt;/h2&gt;

&lt;p&gt;Stripe's API is built for transactions, not analysis. Every time you want to answer a simple question — "which customers signed up this month?" or "what's our MRR?" — you're writing code against a paginated REST API that returns 100 records at a time, enforces rate limits, and requires you to stitch together multiple endpoints to get a complete picture.&lt;/p&gt;

&lt;p&gt;Most teams that need Stripe data in their database end up in one of three places:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom scripts.&lt;/strong&gt; Write a Node or Python script that pulls from the Stripe API and inserts into Postgres. It works until you need to handle incremental updates, schema changes, error recovery, and the ongoing maintenance that comes with any data pipeline. For a side project, it's a weekend. For production, it's a recurring tax on your time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks.&lt;/strong&gt; Stripe pushes events to your server in real-time. Great for triggering actions (send an email when someone subscribes), but terrible for building a queryable dataset. You need to handle event ordering, deduplication, missed events, and design your own table schemas. And if you ever need to backfill historical data, webhooks can't help you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ETL tools.&lt;/strong&gt; Platforms like Fivetran or Airbyte can pipe Stripe data into a warehouse. But they're designed for data teams running Snowflake or BigQuery — overkill and overpriced if you just want a table in your Neon database.&lt;/p&gt;

&lt;p&gt;None of these are bad. They're just heavy for what most developers actually need: their billing data in a Postgres table they can query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Neon Specifically?
&lt;/h2&gt;

&lt;p&gt;If you're already running your application on Neon, there's a strong case for keeping Stripe data there too, rather than spinning up a separate system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JOIN billing with app data&lt;/strong&gt; — match Stripe customers to your own &lt;code&gt;users&lt;/code&gt; table with a single query. No API calls, no stitching responses together in application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query without rate limits&lt;/strong&gt; — once the data is in Postgres, you can run the same query a thousand times without hitting Stripe's API throttling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale-to-zero compute&lt;/strong&gt; — Neon suspends idle compute automatically. If you're syncing once an hour, you only pay for compute during those brief windows — not for 24 hours of uptime. Storage is billed separately, but compute costs stay minimal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database branching&lt;/strong&gt; — want to test a schema change or add a new data type? Create a Neon branch (a copy-on-write clone), run your sync against it, and verify the results before applying changes to production. No risk to your live data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in SQL Editor&lt;/strong&gt; — open the Neon Console, write a query, get answers. No local tooling needed for quick analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole point of choosing a serverless database is simplicity. Building a custom sync pipeline on top of it defeats that purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Steps, No Code
&lt;/h2&gt;

&lt;p&gt;Here's the setup using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt;, which handles the Stripe-to-Postgres pipeline so you don't have to build one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Grab Your Neon Connection String
&lt;/h3&gt;

&lt;p&gt;Start in the &lt;a href="https://console.neon.tech" rel="noopener noreferrer"&gt;Neon Console&lt;/a&gt;. Select your project, click the &lt;strong&gt;Connect&lt;/strong&gt; button, and copy the connection string. It looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://user:pass@ep-cool-name.region.aws.neon.tech/dbname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; When copying the connection string, choose the &lt;strong&gt;pooled&lt;/strong&gt; option (the hostname includes &lt;code&gt;-pooler&lt;/code&gt;). Neon's built-in connection pooler handles sync connections more efficiently, especially if your app is already connected to the same database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Pick Your Data Type
&lt;/h3&gt;

&lt;p&gt;In Codeless Sync, click &lt;strong&gt;Create Sync Configuration&lt;/strong&gt; to open the wizard. Select &lt;strong&gt;Stripe&lt;/strong&gt; as the provider and choose a data type — Customers, Invoices, Subscriptions, and others are available. Customers is a good starting point since it's easy to verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Connect Both Services
&lt;/h3&gt;

&lt;p&gt;The wizard asks for two connections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt; — select &lt;strong&gt;Neon&lt;/strong&gt; as the platform and paste your connection string from Step 1. The connection is tested automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; — enter a restricted API key (&lt;code&gt;rk_test_&lt;/code&gt; or &lt;code&gt;rk_live_&lt;/code&gt;) with read-only permissions. You can create one in the &lt;a href="https://dashboard.stripe.com/apikeys" rel="noopener noreferrer"&gt;Stripe Dashboard&lt;/a&gt; under Developers → API Keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Create the Table
&lt;/h3&gt;

&lt;p&gt;Codeless Sync needs a destination table in Neon. Click &lt;strong&gt;Auto-Create Table&lt;/strong&gt; and the correct schema is created for you. If you'd rather review the SQL first, copy the template and run it in Neon's SQL Editor.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Verify Table&lt;/strong&gt; to confirm the structure is correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Sync and Verify
&lt;/h3&gt;

&lt;p&gt;Name your configuration, click &lt;strong&gt;Create&lt;/strong&gt;, then hit &lt;strong&gt;Sync Now&lt;/strong&gt; from the dashboard. The first sync pulls all matching records — typically a few seconds to a couple of minutes depending on volume.&lt;/p&gt;

&lt;p&gt;Once complete, open Neon's SQL Editor and check:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see your data, you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queries That Make This Worth It
&lt;/h2&gt;

&lt;p&gt;Billing data in Postgres is only valuable if you use it. Here are three examples that show what becomes possible once your Stripe tables live in Neon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find recent signups and flag overdue accounts:&lt;/strong&gt;&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;delinquent&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'overdue'&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'current'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;billing_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Match Stripe customers to your application users:&lt;/strong&gt;&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stripe_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_since&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the query that's nearly impossible through the Stripe API alone — it requires data from two different systems. With both tables in Neon, it's a three-line JOIN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly revenue from paid invoices:&lt;/strong&gt;&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stripe amounts are in cents, so divide by 100 for readable figures. Sync your invoices table the same way you did customers — just pick "Invoices" as the data type in a new configuration.&lt;/p&gt;

&lt;p&gt;All of these run directly against your Neon database. No API calls, no rate limits, no pagination tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping It Fresh
&lt;/h2&gt;

&lt;p&gt;A one-time sync works for exploration, but most teams want current data. You can set up scheduled syncs — hourly, daily, or a custom interval — so your tables stay up to date automatically.&lt;/p&gt;

&lt;p&gt;After the initial full sync, subsequent runs are incremental: only records that changed since the last run are fetched. This keeps sync times short and Stripe API usage low.&lt;/p&gt;

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

&lt;p&gt;If you're running an app on Neon and collecting payments through Stripe, the data from both systems belongs in the same database. The alternative — maintaining webhooks, writing custom scripts, or paying for an enterprise ETL tool — is more complexity than most teams need.&lt;/p&gt;

&lt;p&gt;The setup takes five minutes, the free tier covers manual syncing, and once it's running, your billing data is just another table you can query.&lt;/p&gt;

&lt;p&gt;Give it a try: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/stripe-to-neon" rel="noopener noreferrer"&gt;Sync Stripe Data to Neon — No Code, Auto-Create Tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://codelesssync.com/docs/guides/neon-setup" rel="noopener noreferrer"&gt;Connect Neon to Codeless Sync (Setup Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>neon</category>
      <category>stripe</category>
      <category>nocode</category>
    </item>
    <item>
      <title>Stripe Webhooks vs Database Sync: Which is Better?</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Fri, 27 Feb 2026 19:52:39 +0000</pubDate>
      <link>https://forem.com/ilshadyx/stripe-webhooks-vs-database-sync-which-is-better-4kk4</link>
      <guid>https://forem.com/ilshadyx/stripe-webhooks-vs-database-sync-which-is-better-4kk4</guid>
      <description>&lt;p&gt;&lt;em&gt;Comparing Stripe webhooks and database sync for getting billing data into your app. Learn when to use each approach — and when to use both.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Ilshaad Kheerdali · 27 Feb 2026&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you're building anything on top of Stripe, you'll eventually need to get that data into your own database. There are two main approaches: webhooks and database sync. Both work, but they solve different problems.&lt;/p&gt;

&lt;p&gt;This post breaks down how each approach works, what can go wrong, and when you should use one over the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Webhooks Work
&lt;/h2&gt;

&lt;p&gt;Stripe webhooks are push-based. When something happens in Stripe — a customer signs up, a payment succeeds, a subscription gets cancelled — Stripe sends an HTTP POST request to your server with the event details.&lt;/p&gt;

&lt;p&gt;Here's what a typical webhook handler looks like in Express:&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;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&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;stripe&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;Stripe&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;STRIPE_SECRET_KEY&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;endpointSecret&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;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;switch &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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Insert into your database&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Update your database&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;// Mark invoice as paid&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... handle dozens more event types&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&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;received&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the basic idea. Stripe pushes events to you in near real-time, and your server processes them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems with Webhooks
&lt;/h2&gt;

&lt;p&gt;Webhooks are great in theory. In practice, they come with a list of operational challenges:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missed events.&lt;/strong&gt; If your server is down when Stripe sends an event, you miss it. Stripe retries for up to 72 hours, but if your server has an extended outage or the endpoint URL changes, events get lost permanently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ordering issues.&lt;/strong&gt; Events can arrive out of order. You might receive &lt;code&gt;customer.updated&lt;/code&gt; before &lt;code&gt;customer.created&lt;/code&gt; if there's a slight delay. Your code needs to handle this gracefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No historical backfill.&lt;/strong&gt; Webhooks only capture events from the moment you set them up. If you want last year's customer data, webhooks can't help you. You'll need to write a separate backfill script using the Stripe API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema management.&lt;/strong&gt; Every event type has a different payload shape. Supporting &lt;code&gt;customer.created&lt;/code&gt;, &lt;code&gt;invoice.paid&lt;/code&gt;, &lt;code&gt;subscription.updated&lt;/code&gt;, and dozens of other events means maintaining a lot of mapping logic. And when Stripe updates their API, your handlers may need updating too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay complexity.&lt;/strong&gt; Need to rebuild your data after a bug? You'll have to use the Stripe dashboard to resend events one by one, or write a custom replay script. There's no "sync everything from scratch" button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signature verification.&lt;/strong&gt; Every webhook request needs cryptographic verification. It's one more thing to get right, and getting it wrong means either rejecting valid events or accepting forged ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Database Sync Works
&lt;/h2&gt;

&lt;p&gt;Database sync takes the opposite approach. Instead of Stripe pushing events to you, a sync service periodically pulls data from the Stripe API and writes it directly into your PostgreSQL database.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect your PostgreSQL database (Supabase, Neon, Railway, or any PostgreSQL host)&lt;/li&gt;
&lt;li&gt;Provide your Stripe API key (read-only)&lt;/li&gt;
&lt;li&gt;The sync service calls the Stripe API, fetches your data, and writes it to a structured table&lt;/li&gt;
&lt;li&gt;On subsequent runs, it only fetches records that have changed (incremental sync)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's no webhook handler to write, no event types to map, and no signature verification. The data just shows up in your database, ready to query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side-by-Side Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;th&gt;Database Sync&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data freshness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Near real-time (seconds)&lt;/td&gt;
&lt;td&gt;Batch (hourly/daily)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High — endpoint, verification, event handling&lt;/td&gt;
&lt;td&gt;Low — connect database and API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Historical data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No — only captures new events&lt;/td&gt;
&lt;td&gt;Yes — full backfill on first sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You handle retries and failures&lt;/td&gt;
&lt;td&gt;Managed by the sync service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ongoing — handle new event types, API changes&lt;/td&gt;
&lt;td&gt;Minimal — schema updates handled automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Significant — handler, mapping, error handling&lt;/td&gt;
&lt;td&gt;None (no-code) or minimal (custom scripts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data completeness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only events you subscribe to&lt;/td&gt;
&lt;td&gt;All records for the data type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Triggering actions in real-time&lt;/td&gt;
&lt;td&gt;Querying and analysing data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Webhooks are the right choice when you need to &lt;strong&gt;react to events in real-time&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send a welcome email&lt;/strong&gt; when a customer signs up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provision access&lt;/strong&gt; when a subscription starts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoke access&lt;/strong&gt; when a payment fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger a workflow&lt;/strong&gt; when an invoice is finalised&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update a UI&lt;/strong&gt; in real-time when a charge succeeds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your use case is "when X happens in Stripe, do Y immediately," webhooks are probably what you want. The real-time push model is designed exactly for this.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Database Sync
&lt;/h2&gt;

&lt;p&gt;Database sync is the right choice when you need to &lt;strong&gt;query, analyse, or join Stripe data&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build dashboards&lt;/strong&gt; showing revenue trends, churn, or customer growth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run ad-hoc queries&lt;/strong&gt; like "which customers have spent over £1,000 this quarter?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join billing data with your app data&lt;/strong&gt; — combine Stripe customers with your users table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate reports&lt;/strong&gt; for accounting or investor updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power internal tools&lt;/strong&gt; where teams need to look up customer billing history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example. Once your Stripe data is synced to PostgreSQL, you can run queries like this:&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="c1"&gt;-- Monthly revenue trend for the last 6 months&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_invoices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'6 months'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Customers who signed up but never paid&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;stripe_invoices&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stripe_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try doing that with the Stripe API directly. You'd need multiple paginated API calls, client-side filtering, and careful rate limit handling. With synced data, it's just SQL.&lt;/p&gt;

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

&lt;p&gt;Here's the thing — webhooks and database sync aren't mutually exclusive. In fact, the best setups often use both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; handle real-time events: send emails, update permissions, trigger workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database sync&lt;/strong&gt; keeps a queryable copy of your Stripe data for reporting and analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you the best of both worlds. Your app reacts to events instantly through webhooks, while your team can run any query they want against the synced database. The two approaches complement each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with Database Sync
&lt;/h2&gt;

&lt;p&gt;If you want to try the sync approach, &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt; lets you connect your PostgreSQL database and start syncing Stripe data in about 5 minutes. There's a free tier, no credit card required.&lt;/p&gt;

&lt;p&gt;For a step-by-step walkthrough, check out our tutorial: &lt;a href="https://codelesssync.com/blog/how-to-sync-stripe-data-to-postgresql" rel="noopener noreferrer"&gt;How to Sync Stripe Data to PostgreSQL in 5 Minutes&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://codelesssync.com/stripe-to-supabase" rel="noopener noreferrer"&gt;Sync Stripe Data to Supabase — No Code, Auto-Create Tables&lt;/a&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>How to Sync Stripe Data to PostgreSQL in 5 Minutes (No Code)</title>
      <dc:creator>ilshaad</dc:creator>
      <pubDate>Sun, 04 Jan 2026 10:36:43 +0000</pubDate>
      <link>https://forem.com/ilshadyx/how-to-sync-stripe-data-to-postgresql-in-5-minutes-no-code-82a</link>
      <guid>https://forem.com/ilshadyx/how-to-sync-stripe-data-to-postgresql-in-5-minutes-no-code-82a</guid>
      <description>&lt;h2&gt;
  
  
  A practical guide to getting your Stripe data into your PostgreSQL database without writing webhook handlers.
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to build analytics on your Stripe data, you've probably hit the same wall: the Stripe API is great for transactions, but terrible for queries.&lt;/p&gt;

&lt;p&gt;Want to know which customers signed up last month and haven't made a purchase? That's multiple API calls, pagination handling, and probably some rate limit errors along the way. Meanwhile, the same query in SQL takes about 10 seconds to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Joining Stripe customer data with your own user tables usually means setting up webhooks, designing schemas, and handling edge cases. It can easily take a week of work just to get something basic running.&lt;/p&gt;

&lt;p&gt;There are tools that solve this. In this post, I'll walk through using &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;Codeless Sync&lt;/a&gt;—it connects to your PostgreSQL database and pulls in your Stripe data so you can query it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Walkthrough
&lt;/h2&gt;

&lt;p&gt;Here's how to set it up. The whole process takes about 5 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start the Configuration Wizard
&lt;/h3&gt;

&lt;p&gt;Log into your Codeless Sync dashboard and click &lt;strong&gt;Create Sync Configuration&lt;/strong&gt;. This opens a 5-step wizard that walks you through the setup.&lt;br&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%2Fu08knk1q5abhf26pqbc9.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%2Fu08knk1q5abhf26pqbc9.png" alt="codeless sync step 1"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Select Provider &amp;amp; Data Type
&lt;/h3&gt;

&lt;p&gt;First, you pick what data you want to sync:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider&lt;/strong&gt;: Select Stripe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Type&lt;/strong&gt;: Choose what to sync (Customers, Subscriptions, and more)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd recommend starting with Customers for your first sync—it's usually the simplest to verify.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Connect Your Database
&lt;/h3&gt;

&lt;p&gt;Next, connect to your PostgreSQL database. If you've already added a project, select it from the dropdown. Otherwise, click &lt;strong&gt;New Project&lt;/strong&gt; and enter your connection string.&lt;/p&gt;

&lt;p&gt;If you're using a managed provider, here's where to find your connection string:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt;: Project Settings → Database → Connection string → select &lt;strong&gt;Transaction Pooler&lt;/strong&gt; → copy the URI and replace &lt;code&gt;[YOUR-PASSWORD]&lt;/code&gt; with your database password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neon&lt;/strong&gt;: Dashboard → Connection Details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Railway&lt;/strong&gt;: Your PostgreSQL service → Connect tab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render&lt;/strong&gt;: Database → Info → External Database URL
&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%2Fui9s26nudvqph3qnuec1.png" alt="codeless sync step 3"&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  4. Add Your Stripe Key
&lt;/h3&gt;

&lt;p&gt;Enter your Stripe API key. I'd recommend using a restricted key (&lt;code&gt;rk_test_&lt;/code&gt; or &lt;code&gt;rk_live_&lt;/code&gt;) with read-only access—you don't need write permissions for syncing data out.&lt;/p&gt;

&lt;p&gt;Find your keys in the &lt;a href="https://dashboard.stripe.com/apikeys" rel="noopener noreferrer"&gt;Stripe Dashboard&lt;/a&gt; under Developers → API Keys.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Test Connection&lt;/strong&gt; to validate the key before continuing.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Create the Table
&lt;/h3&gt;

&lt;p&gt;Now you need a table in your database to hold the data. You can either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Auto-Create Table&lt;/strong&gt; to have it created automatically&lt;/li&gt;
&lt;li&gt;Copy the SQL template and run it yourself if you prefer more control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creating the table, click &lt;strong&gt;Verify Table&lt;/strong&gt; to confirm it exists.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Name &amp;amp; Create
&lt;/h3&gt;

&lt;p&gt;Give your configuration a name (something like "Production Customers" or "Test Invoices") and review your settings. Click &lt;strong&gt;Create Configuration&lt;/strong&gt; to finish.&lt;/p&gt;
&lt;h3&gt;
  
  
  7. Run Your First Sync
&lt;/h3&gt;

&lt;p&gt;Back on the dashboard, find your new configuration and click &lt;strong&gt;Sync Now&lt;/strong&gt;. Depending on how much data you have, this could take anywhere from a few seconds to a few minutes.&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%2F6205xfiwi46wpcr2la5a.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%2F6205xfiwi46wpcr2la5a.png" alt="codeless sync step 7"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  8. Query Your Data
&lt;/h3&gt;

&lt;p&gt;Once it's done, open your database client and run some queries:&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="c1"&gt;-- Customers from the last 30 days&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Join with your own users table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stripe_created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your Stripe data is now queryable alongside everything else in your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense
&lt;/h2&gt;

&lt;p&gt;This approach works well when you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run ad-hoc queries against your billing data&lt;/li&gt;
&lt;li&gt;Build reports or dashboards&lt;/li&gt;
&lt;li&gt;Join Stripe data with your own tables&lt;/li&gt;
&lt;li&gt;Avoid hitting Stripe's API rate limits repeatedly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's &lt;strong&gt;not&lt;/strong&gt; a replacement for webhooks if you need real-time event handling (like sending emails when someone subscribes). For that, you still want proper webhook integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping Data Fresh
&lt;/h2&gt;

&lt;p&gt;For one-off analysis, a manual sync is usually fine. If you need the data to stay current, scheduled syncs can run hourly or daily.&lt;/p&gt;

&lt;p&gt;The sync is incremental after the first run, so it only fetches records that have changed since the last sync.&lt;/p&gt;




&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Tf4bRmzbkLA"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Codeless Sync has a free tier if you want to try it out: &lt;a href="https://codelesssync.com" rel="noopener noreferrer"&gt;codelesssync.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>postgres</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
