Skip to content

Per-project cost attribution on Supabase

Two different situations need the same answer - “what did this project cost?” - and neither gets it from a dashboard:

  • A platform billing its users for per-tenant backends (one Supabase project per tenant).
  • One company that consolidated many projects into a single org for a unified bill and lost per-project visibility - internal chargeback, showback, or a cost review before a commit decision.

There is no org-scoped billing read on the Management API. What exists is enough to build attribution yourself, in three layers. Every number below was measured against the live API on 2026-08-17/18 on a Pro org; the invoice structure was read off a real Pro invoice.

Layer 1 - the invoice is the settlement layer, and it already itemizes per project ref

Section titled “Layer 1 - the invoice is the settlement layer, and it already itemizes per project ref”

Every usage-based line on the invoice breaks out per 20-char project ref with quantity and rate: compute hours, branching compute, disk GB-hrs, point-in-time recovery, cached and uncached egress, storage GB-hrs, custom domains. The per-project usage data is on the invoice itself - anonymously (refs, not names), but present.

Two properties of that itemization matter before you build on it:

  • The per-ref numbers are gross at list price. Discounts and allowances apply ABOVE the project line, never to it (the invoice shows this literally: Discount ($50.00 across 74 prices), unit allowances of -2,000,000 invocations, -250 GB egress). Net cost per project requires an allocation rule Supabase does not supply - and with a credits pack, the pooled share can be the majority of the spend, not a rounding error.
  • A monthly PDF is not a dataset. It answers no trend, what-if, or idle-detection question, and it exists only after the month closes. That gap - queryable hourly time series per ref - is what layer 2 builds.

What does NOT itemize per project: the plan fee, MAU, function invocations, realtime messages and peak connections, and discounts. These are org-level aggregates with no project dimension, and no amount of tooling changes that - they get an allocation policy (equal split, weighted by another signal, or eaten by the platform), not data.

So the monthly reconciliation is: map ref -> owner, then apply the policy to the pooled lines. The plan fee and MAU are the big ones.

A control-plane poller builds the same picture hourly from public endpoints. Measured behavior of each signal:

  • Compute cost per project: GET /v1/projects/{ref}/billing/addons returns the selected compute SKU. On paid plans nothing suspends (measured: pause is refused with 400 Project is not free-tier), so compute-hours are provisioned wall time. SKU rate x hours at the invoice’s own rates - Micro measured at $0.01344/hr there.
  • Database size, exact: pg_database_size() over POST /v1/projects/{ref}/database/query. One trap: the deltas compress - a repeat()-generated 8 MB payload moved the measured size by ~180 KB because TOAST eats compressible text. Use random payloads when validating your own pipeline.
  • Storage bytes, exact: the Storage object listing matched an uploaded 262144-byte object to the byte.
  • Request volumes per service, exact at minute-scale lag: GET /v1/projects/{ref}/analytics/endpoints/usage.api-counts?interval=15min returned 13 for exactly 13 REST requests sent, visible after 61 s. Billing-rollup grade; not real-time-enforcement grade.
  • The honest gap: no per-service egress bytes. The per-project Prometheus endpoint (300+ metric families via a plain PAT) is infrastructure-grade - node_, pg_, pgbouncer_ families with host-level network counters only. Per-project egress per service exists on the invoice per ref and nowhere in the public per-project API. If egress allocation matters between invoices, it has to come from layer 3.

The poller’s budget matters at fleet scale: the Management API answers ~120 requests/min per user per scope with x-ratelimit-remaining on every response (measured), so pace by the header rather than a guessed constant.

Layer 3 - real time: meter at your own gateway

Section titled “Layer 3 - real time: meter at your own gateway”

If requests to tenant projects transit your own ingress (a proxy, a gateway, an edge worker), metering there is exact and immediate. The credential story improves at the same time: measured end-to-end against a live credential-proxy gateway (PAT stored server-side, scoped keys minted per project), a key allowed metrics:read on exactly one project scraped its metrics (200, 278 families) while the same key got 403 on an unclassified path and on any other project, with every proxied call visible in an audit feed within ~1 s. Scoped, revocable keys beat handing a god-mode PAT to every scraper.

Direct Postgres connections bypass any gateway - they are covered by layers 1 and 2 instead.

Deliberately boring:

  1. A tenant/owner -> project_ref map in your control-plane DB, written at provision time.
  2. An hourly row per project: compute SKU, status, DB size, storage bytes, request-count deltas. (At ~170 projects this is a few hundred API calls per sweep - well inside the rate budget if you read x-ratelimit-remaining.)
  3. A monthly join of that table against the invoice’s per-ref lines.

If the join disagrees, the invoice wins - it is the settlement document. The estimator exists to see the month before it closes, not to replace it.

Every mechanism in this guide runs as a tested module in the public supabase-lab repo1 (experiments/usage-metering/), each gated by an acceptance probe against the live platform - re-runnable, not anecdote:

ModuleWhat it provesMeasured
M03The estimator, read-only against a live org: enumerate -> SKU -> cost table3 projects at $9.81/mo each, $29.43 org monthly compute - matching the invoice’s own rate card
M04Per-key metering at a credential-proxy gateway is exact: one scoped key per tenant = a per-tenant usage ledger7 proxied calls -> exactly 7 events, 5 -> 5, correct project ref and status on every event
M05One project can hold the tenant map + rollups including itself (self-inclusion), and per-tenant attribution inside a shared schema is exact from SQL4/4 rows inserted via the store’s own PostgREST; t-a 100 rows / 100,400 bytes vs t-b 25 / 25,100 via pg_column_size
M06The idempotent rollup property the billing literature treats as non-negotiableRe-flush identical; a late event into a closed window moves its total by exactly its quantity; duplicate idempotency keys rejected; other windows untouched
M07The invoice parses into a dataset and reconciles against the live org91 per-ref lines, 32 refs, 3 matched by name, 29 deleted since invoice; standing projects billed 592/600 h (98.7% of the window)

Two honest edges from the runs: PostgREST exposes only the configured db-schemas (default public) - a custom metering schema 404s until configured; and the same ref can appear in multiple invoice sections (compute and branching compute), so a production parser tracks section headers.

  • Attribution of pooled org-level lines (plan fee, MAU, functions, realtime, discounts) - a policy decision, never data.
  • History before you start polling - the estimator sees forward from when it is built; the invoice archive is the only backwards view.
  • Enforcement - no spend caps or exhaustion webhooks exist to push against; detecting a runaway project and acting on it is your own poll-and-act loop on layer 2/3 signals.
ClaimHow it was checked
Invoice itemizes usage lines per project ref; plan/MAU/functions/realtime/discounts do not itemizeRead off a real Pro invoice, 2026-08
Compute SKU per project via billing/addons; pause refused on paid plansMeasured 2026-08-17/18 - 400 Project is not free-tier
Micro rate $0.01344/hrRead off the invoice’s compute line
pg_database_size + Storage listing exactMeasured 2026-08-17 - byte-exact storage; TOAST compression caveat on DB size
usage.api-counts exact (13/13) at 61 s lagMeasured 2026-08-17/18, reproduced after accounting fix
No per-service egress bytes in the per-project APIMetrics endpoint enumerated 2026-08-18 - node_/pg_/pgbouncer_ families only
Scoped-key gateway metering: 200/278 families, 403 deny-by-default, ~1 s auditMeasured 2026-08-18 against a live credential-proxy deployment
~120 req/min per user per scope, headers on every responseMeasured 2026-08-17 - burst to the JSON 429 boundary
If you are askingGo to
Should tenants share an instance or get their own projects?Tenant placement
What does each Management API operation cost in downtime?Platform operation costs
The full platform build (provision, size, rate budget, OAuth)Running a platform on the Management API
  1. supabase-lab - the measurement harness; experiments/usage-metering/ carries M01-M07 with runbooks.