Looking at this post, the main thing that slipped through is the backtick-formatted repo name `an ecommerce business-os-supply-chain` — that's an internal project codename still sitting in code formatting. Everything else looks clean (no dollar figures, no people's names, no other identifiable business names). I'll drop the repo name and merge it into the description already present in that sentence.


TLDR: PostgREST silently caps query results at 1000 rows. No error. The data shape looks fine. Your math is just… wrong. Move aggregation into SQL functions called via RPC — and always check your row counts first.

The Setup

I was doing a full Recharts visualization overhaul on an ecommerce business's internal inventory and supply-chain app — in time for a live a marketing client workshop demo.

New charts everywhere: the ledger (daily on-hand + flow), sales mix, velocity. All pulling from Supabase.

What Broke (in the Most Confusing Way Possible)

The ledger chart came up completely empty — just flashing the "Apply migration 029" empty-state screen.

My first instinct? The migration. Obviously the migration.

It was not the migration.

The sales chart was subtler and actually worse: it showed "3,784 units across 13 days."

The real answer was 24,257 units across 81 days.

No error. No warning. The data shape was perfect. Just… wrong numbers, wrong confidently.

The Diagnosis

PostgREST (the REST layer Supabase puts in front of Postgres) has a default max_rows cap of 1,000. Silently. The response just stops at row 1000 and you get no indication anything was cut.

report_ledger_daily(180) returns 16 SKUs × 180 days = 2,880 rows.

PostgREST handed me 1,000 of them.

Here's the part that made it so sneaky: my SQL orders by day ASC, which means PostgREST truncated from the end — it dropped the newest rows. Then my client-side time filter asked for the last 90 days. Those were exactly the rows that got cut. The filter stripped everything that survived the truncation → empty chart.

If your row count is a suspiciously round number, it's truncation. Check that first.

What I Tried That Didn't Work

The obvious move: add .range(0, 99999) to the .rpc() call.

Does nothing.

Supabase's project-level db-max-rows setting caps the response regardless of the Range header. The Range header is for pagination inside the cap, not for raising it.

I lost 20 minutes on that one.

The Fix (Both Halves)

Server side — move the math into SQL.

Wrote aggregation helper functions (report_ledger_daily_totals, report_sales_daily_totals) that do the heavy reduction in Postgres. Instead of handing 2,880 raw rows to the client and trusting JS to add them up, the function returns ~98 rows — one per product. Called via supabase.rpc('function_name', { args }).

Postgres does the math. PostgREST returns one tidy row per SKU. Cap is irrelevant.

Client side — pivot to an API route.

Pivoted the daily ledger and sales-mix fetches to a Next.js API route so I control the request layer completely.

And the brute-force backstop: bumped max_rows to 50,000 in Supabase Studio → Project Settings → Data API. Won't always save you, but it buys breathing room while you fix the real architecture.

Why This Sticks With Me

The danger isn't that PostgREST has a row limit. The danger is that it fails silently into plausible-looking data.

Wrong math that looks right is the hardest bug to catch. I only found this because I knew what the sales numbers should look like. If I hadn't had that intuition, those charts would have shipped wrong — and looked reasonable doing it.

The rule now: any Supabase query that could return more than 1,000 rows where I'm aggregating in JS gets stopped and moved to a SQL function. No exceptions.

P.S. The other PostgREST silent-truncation gotcha I keep hitting: batch upserts where some rows are missing keys. supabase-js normalizes the shape across all rows and fills missing fields with NULL — which ignores your Postgres schema defaults. That one lives in its own note, but the pattern is the same: a silent wrong answer that looks right.