DEV Community

Cover image for How to Sync Billing Data to Supabase (Stripe, QuickBooks, Xero & Paddle)
ilshaad
ilshaad

Posted on Originally published at codelesssync.com

How to Sync Billing Data to Supabase (Stripe, QuickBooks, Xero & Paddle)

Sync Stripe, QuickBooks, Xero or Paddle into Supabase Postgres in about 5 minutes. Auto-created tables, scheduled syncs, and the RLS step most guides skip.

By Ilshaad Kheerdali · 5 October 2026


Supabase is where a lot of small SaaS teams keep their application database, so it is the obvious place to put billing data too. One database, one SQL console, and your Stripe invoices sitting next to your own users table so you can join them.

Getting the data there is the part people underestimate. Supabase ships an official Stripe integration, which is genuinely good and which this post will tell you to use in some cases. What it will not do is bring in QuickBooks, Xero or Paddle, and most teams past their first year have at least one of those in the mix: Stripe for revenue, an accounting platform for everything the accountant needs.

This post covers getting all four into a Supabase project, what the tables look like, what it costs, and the one security step that catches almost everyone.

Why Supabase for Billing Data

Supabase is plain Postgres with a good dashboard on top, which is exactly what billing data wants. No proprietary query dialect, no warehouse loading step, no waiting on a nightly job before you can run a query.

The practical wins:

  • You can join billing data to your own tables. Revenue per signup cohort is one query when stripe_subscriptions and your users table live in the same database. It is a data export and a spreadsheet when they do not.
  • The SQL editor is already open. You do not need a BI tool to answer "how much did we invoice in August".
  • Row Level Security and the auto-generated API come free. Once the data is in, a read-only dashboard is a policy and a select, not a backend.
  • The free tier is real. 500 MB of database space is far more than a billing sync needs at small scale.

If you are still deciding where to host, Supabase vs Neon vs Railway compares the three on exactly this workload.

Supabase Already Has a Stripe Integration, Should You Use It?

Yes, sometimes. Being straight about this matters more than winning the paragraph.

Supabase has two official routes for Stripe:

The Stripe Sync Engine. A one-click integration in the Supabase dashboard that keeps a set of Stripe tables current using webhooks plus scheduled backfills. Supabase configures both for you. The project moved to Stripe's own GitHub organisation in April 2026 and is now maintained jointly, so it is not going anywhere.

The Stripe foreign data wrapper. This one gets confused with a sync, and it is not one. The Supabase FDW docs describe it as reading data from Stripe within your Postgres database: rows are fetched live from the Stripe API every time you query. It covers 24 Stripe objects, only about three of which support writes, and the docs list the trade-offs plainly. Large result sets are slow because all the data has to transfer, webhook events and real-time updates are not supported, and materialized views built on foreign tables can fail during logical backups.

So the honest guidance:

Your situation Use
Stripe only, Supabase only, happy running webhooks in your project Supabase's Stripe Sync Engine
Occasional lookups, small result sets, no need for local rows The Stripe FDW
You also need QuickBooks, Xero or Paddle in the same database Codeless Sync
You want the same sync tooling across Supabase, Neon, RDS and elsewhere Codeless Sync
You want no webhook endpoint to own at all Codeless Sync

The gap is not quality, it is coverage. The official integration is Stripe to Supabase and nothing else. If your accountant works in Xero and your revenue runs through Stripe, you need both in one database, and that becomes a two-tool problem the moment you pick the Supabase-only option.

Connecting Supabase to Codeless Sync

Two ways in, and both end at the same place: an encrypted connection Codeless Sync uses for writes.

OAuth. Click Connect Supabase, approve the scopes, pick the project. Nothing to copy, no password to handle. This is the path to take, and it is covered in detail in Sync Supabase via OAuth.

Connection string. Grab the pooler string from Project Settings → Database, swap [YOUR-PASSWORD] for the real password, and hit Test & Connect. Worth knowing that Supabase gives you a transaction-mode pooler string on port 6543 and a session-mode one on 5432, and that direct connections are IPv6 only on newer projects unless you have the IPv4 add-on. If a string refuses to connect, this troubleshooting guide walks through the usual causes.

From there the flow is the same whichever provider you are pulling from. Authorise the source, pick the records you want, and Codeless Sync creates the destination table and fills it. QuickBooks and Xero connect over OAuth. Stripe and Paddle take an API key, and both support restricted read-only keys, which is what you should give it.

Each provider has its own setup page with the exact steps: Stripe to Supabase, QuickBooks to Supabase, Xero to Supabase and Paddle to Supabase.

What the Tables Look Like

Codeless Sync creates the tables for you, so this is reference rather than homework, but you should know the shape before you start writing queries against it.

Every table follows the same pattern. The fields you actually filter and group by get real typed columns, the full API object goes in a data JSONB column so nothing is lost, and there is sync metadata on the end.

CREATE TABLE IF NOT EXISTS stripe_customers (
  -- Core queryable fields
  id TEXT PRIMARY KEY,
  email TEXT,
  name TEXT,
  description TEXT,
  currency TEXT,
  balance INTEGER DEFAULT 0,
  delinquent BOOLEAN DEFAULT false,

  -- Fields
  data JSONB,

  -- Sync metadata
  livemode BOOLEAN NOT NULL,
  created TIMESTAMPTZ NOT NULL,
  synced_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_stripe_customers_email ON stripe_customers(email);
CREATE INDEX IF NOT EXISTS idx_stripe_customers_created ON stripe_customers(created);
CREATE INDEX IF NOT EXISTS idx_stripe_customers_livemode ON stripe_customers(livemode);
Enter fullscreen mode Exit fullscreen mode

The livemode flag is worth noticing. Test and live data land in the same table and stay distinguishable, so you can sync a sandbox first and filter it out later.

Xero and QuickBooks tables carry one extra wrinkle, because both can hold more than one company under a single connection. Xero tables use a composite primary key:

CREATE TABLE IF NOT EXISTS xero_invoices (
  id TEXT NOT NULL,
  tenant_id TEXT NOT NULL,              -- Xero organisation ID
  invoice_number TEXT,
  type TEXT,                            -- ACCPAY (bills) or ACCREC (sales)
  contact_id TEXT,
  contact_name TEXT,
  invoice_date DATE,
  due_date DATE,
  status TEXT,
  total NUMERIC(15,2) DEFAULT 0,
  amount_due NUMERIC(15,2) DEFAULT 0,
  amount_paid NUMERIC(15,2) DEFAULT 0,
  currency_code TEXT,
  data JSONB,
  livemode BOOLEAN NOT NULL,
  updated_date_utc TIMESTAMPTZ,
  synced_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
  PRIMARY KEY (id, tenant_id)
);
Enter fullscreen mode Exit fullscreen mode

Always filter on tenant_id when you query Xero tables, or you will double-count across organisations. Syncing multiple Xero organisations covers that pattern properly. For the reasoning behind the typed-columns-plus-JSONB split, see designing a Postgres schema for Stripe data.

Turn On RLS Before You Sync Anything

This is the Supabase-specific step, and skipping it is the one mistake in this post that can actually hurt you.

Supabase exposes the public schema through an auto-generated REST API. Your synced billing tables land in public. Supabase's own Row Level Security docs put it bluntly: a table in an exposed schema without RLS is readable and writable by any role with a grant on it. Your anon key ships in your frontend bundle. Do the arithmetic.

Codeless Sync creates tables with indexes, not with policies, because guessing at your access model would be worse than leaving it to you. So after your first sync, lock the tables down:

-- Take away the default client grants first
REVOKE ALL ON stripe_customers FROM anon, authenticated;

-- Then turn RLS on
ALTER TABLE stripe_customers ENABLE ROW LEVEL SECURITY;
Enter fullscreen mode Exit fullscreen mode

Repeat per synced table. Supabase's docs make the order matter: adding policies does not remove existing grants, so revoke first, then enable RLS, then grant back only what a client genuinely needs.

For most billing tables, the right answer is that no client role needs anything. You query them from the SQL editor or from a server-side process using the service role key, which bypasses RLS by design. If you do want a dashboard reading from them directly, grant SELECT back to authenticated and write a policy that scopes rows to the signed-in user.

Will It Fit on the Free Tier?

Usually yes, and for longer than you would guess.

Supabase's free tier gives you 500 MB of database space, 5 GB of egress and up to two active projects, with projects pausing after a week of inactivity. The Pro plan is $25 a month for 8 GB of disk, 250 GB of egress and $10 of compute credits.

Billing rows are small. A stripe_customers row with its JSONB payload runs roughly 2 to 4 KB, and invoices a little more because the line-item data is fatter. Call it 3 KB average and 500 MB holds somewhere around 150,000 rows before you are near the ceiling. A business invoicing a few hundred customers a month will not approach that for years.

Two things actually push people off the free tier, and neither is the billing sync:

  • Project pausing. A week of no activity and the project sleeps. A scheduled sync counts as activity, so in practice a running sync keeps the project awake.
  • Your application data. The billing tables are rarely what fills 500 MB. Your own tables are.

On the Codeless Sync side, the free plan runs 2 manual syncs a day up to 1,000 rows a month across a fixed set of starter templates: Stripe customers and invoices, QuickBooks customers and invoices, Xero contacts and invoices, Paddle customers and subscriptions. That is enough to prove the setup works on real data. Beyond that you are into the paid tiers, which is also where scheduling lives.

Keeping It Current

Manual syncing is fine while you are testing and tedious after that. Scheduling is what turns this into something you stop thinking about.

Codeless Sync scheduling by plan:

Plan Price Schedules Rows per month Syncs per day
Free $0 Manual only 1,000 2
Starter $19 Daily, weekly, monthly 25,000 10
Pro $29 Daily, weekly, monthly 100,000 40
Business $99 12-hourly, daily, weekly, monthly 500,000 100

Daily is the right default for almost everyone. Billing data is not a real-time signal: an invoice finalised at 3pm does not become more actionable if it lands in your database at 3:05 rather than overnight. How often should you sync billing data makes the case at length.

The part that matters for a Supabase project specifically is that there is no webhook endpoint in it. No route to keep alive, no signature verification, no replay handling, no edge function to debug at midnight. Codeless Sync pulls on the schedule and writes to your database, so those failure modes stay on our side of the line.

What This Costs End to End

For a small SaaS syncing Stripe and Xero into one Supabase project on a daily schedule:

  • Supabase Pro: $25 a month, or $0 if the free tier still fits, which it often does
  • Codeless Sync Starter: $19 a month

So $19 to $44 a month, flat, with no line item that scales with row count. Compare that to usage-priced ETL, where the same two sources on a daily schedule puts you into Fivetran's MAR billing and a bill that moves every month for no change in what you asked for.

And compare it to the build: a webhook endpoint per provider, OAuth token refresh for the accounting platforms, retry and rate-limit handling, and a schema that needs revisiting every time an API adds a field. The hidden cost of rolling your own prices that out honestly.

Frequently Asked Questions

Can I sync more than one billing provider into the same Supabase project?

Yes, and it is the main reason to use Codeless Sync over Supabase's own Stripe integration. Each provider gets its own prefixed tables (stripe_, quickbooks_, xero_, paddle_) so nothing collides, and they all sit in the same database ready to join.

Should I use Supabase's Stripe Sync Engine instead?

If Stripe is your only source and Supabase is your only database, it is a reasonable choice and it is free. Codeless Sync makes more sense when you need a second provider, when you want the same tooling across more than one Postgres host, or when you would rather not run a webhook endpoint at all.

Does Codeless Sync enable Row Level Security on the tables it creates?

No. Tables are created with indexes but without policies, because the right access model depends on your app. Since Supabase exposes the public schema through its auto-generated API, revoke the anon and authenticated grants and enable RLS on every synced table after your first sync.

Will syncing wake up a paused Supabase project?

A scheduled sync counts as database activity, so a project on a daily schedule will not hit the free tier's one-week inactivity pause. A project that is already paused needs restoring from the dashboard before a sync can write to it.

Do I need the IPv4 add-on?

Not if you use the pooler connection string, which is what the OAuth flow and the dashboard both hand you. Direct connections on newer Supabase projects are IPv6 only, which is where the IPv4 add-on comes in, but the pooler sidesteps it.

Can I give Codeless Sync a read-only Stripe key?

Yes, and you should. Stripe restricted keys let you grant read access to just the resources you are syncing. The same applies to Paddle. Xero connects over OAuth with read-only scopes. QuickBooks also uses OAuth, but Intuit offers no read-only accounting scope, so Codeless Sync requests the standard accounting scope and only ever reads with it. Is it safe to connect Stripe to a third-party sync tool covers the full security posture.

What happens when Stripe adds a field to its API?

It lands in the data JSONB column automatically, so nothing breaks and nothing is lost. If it turns out to be a field worth querying often, it gets promoted to a typed column in a template update on our side, not yours.

Wrapping Up

If you are on Stripe alone, Supabase's own integration will serve you well, and there is no sense pretending otherwise. The moment a second provider enters the picture, or a second database host, you want one tool that covers all of it, and that is the gap Codeless Sync fills.

Connect a Supabase project, authorise Stripe, QuickBooks, Xero or Paddle, and the tables exist with data in them in about five minutes. Then enable RLS on them before you forget.

Start free: codelesssync.com

Top comments (0)