> ## Documentation Index
> Fetch the complete documentation index at: https://supaschema.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Case study

> supaschema measured against a production multi-tenant Supabase platform: ~30 schemas, ~8,300 objects, hundreds of RLS policies.

supaschema was built while developing a production multi-tenant SaaS on Supabase.

The schema had:

* roughly 30 schemas;
* about 8,300 schema objects;
* hundreds of Row Level Security policies.

Every schema edit meant waiting for a shadow-database diff before migrations and generated types could catch up.

All numbers below are reproducible. Object names are kept generic; the measurements are from the real tree.

## Speed: the whole tree, no database, in under two seconds

Extracting and planning the entire declarative tree (8,271 modeled objects) runs on the parser alone — no Docker, no shadow database, no introspection:

```text theme={null}
extract-from 1012ms · extract-to 876ms · plan 5ms
```

A full diff is two extractions plus a plan — **\~1.9 seconds over the entire production tree.** The equivalent Supabase CLI diff replays all 8,300 objects into a fresh Docker shadow database on every run, which is minutes at this scale (see the [benchmarks](/docs/benchmarks#speed-and-accuracy)).

The 8,271 figure is what supaschema models. Supabase-managed declarations are excluded through `schemas.exclude`, role/control-plane bootstrap files under `_bootstrap` are skipped as inventory, and the remaining normalize-fidelity warnings preserve source SQL without changing object identity. None are engine errors.

## Head-to-head on real schema (bounded slice)

A 282-object slice of the real tree (three schemas: identity, calculators, messaging — 73 RLS policies among them) was diffed against itself plus a small additive change, supaschema versus the default Supabase CLI engine, one iteration, both applied to a throwaway Postgres and re-applied:

|                                                 | supaschema | Supabase CLI (default)       |
| ----------------------------------------------- | ---------- | ---------------------------- |
| Diff latency                                    | **361 ms** | 35,293 ms (\~**98× slower**) |
| Migration applies once → target catalog         | yes        | yes                          |
| Migration applies **twice** (crash-safe re-run) | **yes**    | **no**                       |
| Reaches the target catalog after re-run         | **yes**    | **no**                       |

The Supabase engine emitted an unguarded `CREATE TABLE public.…(…)` (no `IF NOT EXISTS`) and an unguarded `CREATE INDEX`, so the second apply fails with `relation "…" already exists`. supaschema's output is guarded by construction (`IF NOT EXISTS`, catalog-checked `DO` blocks), so a crashed or retried deploy simply runs the file again.

Reproduce against any declarative tree:

```bash theme={null}
node benchmarks/tools/build-project-fixture.mjs --tree <your supabase/schemas> --out benchmarks/fixtures/project
SUPASCHEMA_COMPARE_FIXTURES=project \
SUPASCHEMA_COMPARE_TOOLS=supaschema-file,supabase-default \
SUPASCHEMA_COMPARE_ITERATIONS=1 \
SUPASCHEMA_COMPARE_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres \
node benchmarks/compare.js
```

## Security: the miss that speed hides

Speed is the visible pain. RLS correctness is the bigger risk.

On a multi-tenant platform, a policy's `USING` predicate is the tenant boundary.

Changing `USING (true)` to `USING (tenant_id = current_tenant())` can close an isolation hole. Every Supabase CLI diff engine measured here silently drops that policy-body change because it diffs policies by name, not by body.

See the [accuracy results](/docs/benchmarks#accuracy). supaschema scores F1 `1.000`; the compared engines score `0.982-0.999` on the same missed-policy fixture.

A slow diff costs time.

An unreplayable diff costs a deploy.

A silently dropped policy change can ship a tenant-isolation bug.

## Why both speed and security

The two are the same wedge from different ends. The parser-based, no-database design is what makes supaschema fast; that same AST-based identity is what lets it compare policy bodies structurally instead of by name, which is what catches the isolation regression. You do not trade one for the other.
