One Supabase Project per Customer: Apply a Template, Catch Hand-Made Changes With a Schema Fingerprint, Hand Over, and Split the Costs
DEV Community

One Supabase Project per Customer: Apply a Template, Catch Hand-Made Changes With a Schema Fingerprint, Hand Over, and Split the Costs

If you give every customer their own Supabase project, the hard part is not creating them. It is keeping them the same, knowing who owns each one, and handing one over.

I tried the first two on two free projects and the third in a limited way, then built a small console page. Everything is from my run on 2026-10-27, and prices change.

How this was made: I wrote this with Claude's help: it drafted, and I ran every step, edited the text, and checked the results. The mistakes are mine. This is a personal learning project, not an official guide, and I don't work for Supabase.

The non-technical version (two owners, drift, hand-over checks): https://medium.com/p/50275a612b87

The code: https://github.com/tomozilla/customer-env-console-en-demo

What I did and did not do

  • I applied one SQL template to two free projects, compared a schema fingerprint, made a hand change, and applied a second version to one project. $0.
  • Not tested: applying the template with the Supabase CLI migrations (db push needs the database password, which I don't handle), a real hand-over to a customer's organization (in my topic 08 article I only moved a project between two of my own free organizations), roles, and scoped support access (Team plan).
  • Customer names in the repo and screenshots are fictional.

Two owners

  • A: you own it. One project per customer inside your organization. The bill is yours and you manage everything.
  • B: they own it. The project lives in the customer's organization. You are a member with access. You can also start in A and transfer the project later.

Cost (pricing page and docs, 2026-10-27):

  • A: on a paid organization, each running project adds about $10 a month (Micro compute), and the plan includes $10 of compute. Ten customers on Pro: $25 + $100 - $10 = $115 a month. Paused projects aren't charged.
  • B: your cost is $0. The customer pays under their own plan.

A template, applied twice

I put the blueprint in a SQL file, in its own schema portal, written to be safe to run twice:

create schema if not exists portal;
create table if not exists portal.template_meta (version int primary key, applied_at timestamptz not null default now());
alter table portal.template_meta enable row level security; -- I forgot this at first
create table if not exists portal.tickets ( ... );
alter table portal.tickets enable row level security;
create policy "tickets read" on portal.tickets for select to authenticated using (true);
create policy "tickets insert" on portal.tickets for insert to authenticated with check (true);
insert into portal.template_meta (version) values (1) on conflict do nothing;

The full file is sql/01_template_v1.sql.

The "any signed-in user can read" policy is only safe because each project serves one customer. In a shared project you would filter by a customer id with RLS (my topic 03 article).

Stumble: the dashboard warned me. My first version left template_meta, the table that records the version, without RLS. The SQL editor stopped me: The portal schema isn't exposed through the API, so it likely couldn't be reached anyway, but I didn't want to rely on "likely." I added the enable row level security line and ran it again.

Are they really the same? A fingerprint

I wrote a query that turns the columns, RLS flags and policies of portal into one hash. It reads metadata only, never customer data:

select 'v' || coalesce((select max(version) from portal.template_meta), 0) as template_version,
left(md5(coalesce(string_agg(x, '|' order by x), '')), 10) as fingerprint,
count(*) as parts from ( ... strings for columns, RLS flags and policies ... ) t;

Full file: sql/10_fingerprint.sql. I keep the first 10 characters because the full hash was cut off in the results grid.

I applied v1 to two projects, my demo project and a second one I resumed from a paused state: Same fingerprint, 6c935b9a23. The two copies match.

A hand-made change gets caught

In the second project I added a column, the kind of quick fix someone makes for one customer:

alter table portal.tickets add column note text;

It still says v1, but the fingerprint changed and the parts went from 10 to 11. A project that claims v1 but doesn't have v1's shape was touched by hand.

An old version gets caught

I applied v2 (adds a priority column) to the first project only: Latest is ab1a7ecec4. A customer whose fingerprint matches v1 but not v2 is behind.

Handing over

To make a project customer-owned, use Transfer project (I tried it between two of my own free organizations in the topic 08 article):

The docs say (not tested with a customer):

  • You must be an Owner of the source organization and at least a member of the target.
  • No active GitHub integration, project-scoped roles (Team and Enterprise) or log drains.
  • Usage is billed to the source organization until the transfer and to the target after.
  • Your role in the target may differ, so decide the support door first.

The console page

A static page reads a registry (owner, state, template version, fingerprint, last checked) and judges each row. No server, no keys, and it never connects to a project: you run the fingerprint SQL yourself and paste the result into the registry.

The rules:

  • No fingerprint or version: not checked.
  • The fingerprint doesn't match its own version's: changed by hand.
  • Matches the latest version: current. Matches an older one: behind.
python3 -m http.server 8000 # from the repository root, then open http://localhost:8000/app/
node app/test.js # 15 checks

I loaded five fictional customers:

  • acme-demo matches v2: current.
  • globex-demo says v1 but has the hand-made column's fingerprint: changed by hand.
  • initech-demo is a clean v1: behind.
  • umbrella-demo is customer-owned and handed over, no fingerprint yet: not checked.
  • hooli-demo is paused and its check is old: behind, flagged stale check.

My cost: 3 running projects that I own, so $25 + (3 x $10 - $10) = $45 a month. The customer-owned and paused ones aren't counted. I checked this by hand and it matched.

Stumbles

A bad row should not vanish quietly

A row whose owner is neither us nor client is skipped with a visible message, not dropped silently: When that was the only row, the board went empty, which looks like "everything disappeared". The warning is what tells you why.

The sample goes stale

The sample's checkedAt is 2026-10-27, so 30 days later every row shows "stale check". That's how it is meant to work, but a demo will look noisy.

Limits

A fingerprint covers the shape only: no data, and nothing outside portal. It does not say who changed something or why. I paste fingerprints by hand. Reading them automatically would need access to every project, which I chose not to build. Not tested: dozens of projects, phone width, opening the page as a file.

What it costs

This walkthrough and the console: $0 (two free projects). Owner A: plan fee + running projects x about $10 - $10 included. Owner B: $0 to you. Nothing here was checked against an invoice.

Cleanup

I dropped the portal schema in both projects (sql/99_cleanup.sql) and paused the second project again.

If I took this further

Not tried: scheduling the fingerprint query (for example with pg_cron) and writing results to the registry, showing which columns differ, and keeping the registry in Git so changes are traceable.

Takeaways

Own it as A (yours) or B (theirs) by who handles contract and operations. Cost follows the owner. A fingerprint of the schema's shape tells you same, hand-changed, or behind. Obey the dashboard's RLS warning, even for a schema you think is hidden. "Can be moved" is not "safe to hand over."

Next in this series: backups and point-in-time recovery.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.