Skip to main content
ASoc
Comparison

Supabase vs MongoDB: The Question Isn't the App, It's the Dataset

Our 8,114-line, 111-product catalog is document-shaped — and lives in a typed array, not a database. The commerce layer is relational, on Postgres. Why the split decides, not the app.

The ASoc Team8 min read

Supabase wins when your data is relational and improves under constraints — orders that belong to customers, entitlements that gate a download, a schema you want the database to enforce. MongoDB wins when your data is naturally document-shaped and its structure changes faster than a migration file can track it. Most "which database" posts stop there. This one measures which kind of data a real storefront actually has, because the answer split our own catalog in two.

The decision in one table

AxisSupabase (Postgres)MongoDB
Data modelRelational, fixed schema per tableDocuments, schema-optional
Best fitData with real relationships — orders, accounts, entitlementsData with real variance — catalogs, logs, content trees
Query modelSQL, joins, views, aggregatesQuery language + aggregation pipeline, no joins across collections
Enforcing structureThe database rejects a bad row (constraints, types, RLS)The application enforces it, or nothing does
AuthorizationRow-Level Security, evaluated by the database on every connectionApplication-layer, or a separate rules product
Schema changeA migration file, reviewed and versionedAdd a field to new documents; old ones don't have it
Scaling shapeVertical, plus read replicasHorizontal sharding, built for it
Local devReal Postgres, full stackLocal server or a hosted free tier
Where we use itOrders, accounts, entitlements, download grantsNowhere — see below

If you read one row: enforcing structure. It is the row that decided our own architecture, and it is the row most comparisons skip in favor of "SQL vs NoSQL" as an abstraction.

The comparison usually asks the wrong question first

"Supabase vs MongoDB" is normally framed as one database for the whole application. That framing hides the actual decision, which is per dataset, not per app. A storefront has at least two datasets with opposite shapes, and ours is a clean example because both sides are real and both are large.

The catalog — 111 products, each with editions, screenshots, a changelog, an optional gallery, an optional "what's inside" inventory of named groups — is the dataset MongoDB's pitch is built for. Products are heterogeneous: a free product has a githubUrl and no whatsInside; a four-edition admin product has whatsInside groups a single-page landing template never needs. That is exactly the "schema varies per document" shape a relational table fights.

The commerce layer — orders, entitlements, download grants, accounts — is the dataset Supabase's pitch is built for. An order belongs to exactly one customer. An entitlement slot either covers a download or it does not. These are relationships with integrity rules a database should refuse to violate, not conventions an application remembers to check.

We run Postgres for the second dataset and TypeScript for the first — not MongoDB for either. The rest of this post is why, measured against our own repo rather than restated as advice.

Why the catalog isn't in a database at all

src/data/catalog.ts is 8,114 lines: one TemplateProduct per product, in a single typed array, checked into git and compiled into the app at build time.

export interface TemplateProduct {
  slug: string;
  name: string;
  seoLabel: string;
  category: ProductCategory;
  status: ProductStatus;
  pricing: ProductPricing;
  tagline: string;
  description: string;
  features: string[];
  screenshots: string[];
  gallery?: string[];
  whatsInside?: WhatsInsideGroup[];
  editions: Edition[];
  githubUrl?: string;
  relatedSlug?: string;
  latestVersion: string;
  changelog: ChangelogEntry[];
}

That is a document. Nested arrays, optional fields, a shape that legitimately differs between a free single-page template and a four-edition 135-page admin dashboard. If this catalog needed to be edited by non-engineers at runtime — a merchandising team adding products through a CMS — a document store would be a reasonable answer, and the schema-optional model would earn its keep.

It doesn't need that, and the reason is the second half of the argument MongoDB's own pitch usually skips: schema flexibility has to be enforced by something. MongoDB's answer is "your application code, or nothing." Ours is TypeScript's structural typing at compile time, plus 20 invariant tests in src/data/__tests__/catalog.test.ts that run on every push — among them: every slug is unique and URL-safe, every seoLabel is well-formed and doesn't collide with a category hub's target term, every screenshot file referenced actually exists under public/, every gallery slide is a real, distinct WebP file, every whatsInside group (when present) is non-empty. A document database would accept a product missing its cover image and find out in production. Ours fails npm test.

The honest tradeoff: our invariants are less expressive than MongoDB's validators can be (a JSON Schema validator can express roughly the same constraints), and our catalog can't be edited without a code deploy. For 111 products maintained by the team that builds the site, "requires a deploy" is a feature — it means every catalog change goes through the same review and CI as a code change. For a marketplace where products are added by third-party sellers with no engineering access, that call reverses, and a document database with server-side validation is the right answer.

Where we do use Postgres, briefly

The commerce layer is the dataset that actually needed a database, and it's the subject of a separate post rather than repeated here: Supabase vs Firebase for a template-based SaaS covers the row-level security posture (read-own-only, zero write policies, every write through a SECURITY DEFINER RPC), the getClaims()-not-getSession() rule, and the measured ~68 KiB gzipped cost of shipping an auth SDK in a shared layout. What's relevant here is narrower: eight migration files in supabase/migrations/, each a reviewed SQL diff, each one a decision MongoDB would have pushed into application code instead — alter table, not "the next document that omits this field breaks silently."

Where MongoDB is the right call, not a consolation prize

This isn't close in either direction; the two products solve different problems.

  • Content that changes shape faster than you'd want to write migrations. A CMS-backed blog with per-post custom fields, a form builder storing arbitrary field configurations, an activity feed with a different payload per event type — all genuinely document-shaped, and a relational table for any of them means either a wide table of mostly-null columns or an EAV pattern that reinvents documents badly.
  • High-write, high-volume logs and events. Application logs, analytics events, IoT telemetry — write-heavy, rarely joined, and MongoDB's sharding is built for exactly this scale in a way Postgres's read-replica model isn't.
  • A catalog that actually is user- or seller-generated. If products come from hundreds of independent sellers with no shared schema discipline, MongoDB's per-document flexibility beats forcing every seller's data through one relational shape.
  • Teams that already run Mongo in production. Operational familiarity is a real cost. If your team has years of MongoDB Atlas experience and no Postgres experience, that outweighs a lot of architectural argument for a first release.

Mistakes and how they show up

MistakeWhat happensFix
Modeling a relational join as embedded documentsUpdate anomalies — the same fact duplicated across documents driftsReference and query twice, or use Postgres
Treating "schema-optional" as "no schema"A typo'd field name silently creates a new field instead of erroringAdd server-side JSON Schema validation, or use a typed language for the write path
Putting a heterogeneous but fixed-cardinality catalog in Postgres with 40 nullable columnsAn unreadable table, and every new product type needs a migrationA typed array (build-time content) or a jsonb column (runtime content) — not more columns
Assuming a document database needs no invariant checksA product ships with a missing cover image and nobody notices until a 404Write the checks yourself — validators, or tests, whichever your stack runs on CI
Choosing per app instead of per datasetOne database forced onto two different data shapesAsk "does this table have real relationships" for every table, not once for the app

Frequently asked questions

Can Supabase handle document-shaped data at all? Yes — Postgres's jsonb column type stores and indexes JSON natively, and it's a reasonable middle ground when you want relational guarantees on most columns and flexibility on one. It's not a replacement for MongoDB's aggregation pipeline on deeply nested, high-write document workloads, but for "this table has one field that varies," jsonb is usually the simpler answer than a second database.

Why not just put the catalog in Postgres too, since you already run it? We considered it. The catalog is read-only at request time — it's compiled into the app at build time, so there's no runtime write path to protect and no reason to pay a network round-trip per product lookup. A typed array is faster to read, free to query (it's just JavaScript), and the invariant tests give us most of what a schema would, without standing up a table for content that never changes outside a deploy.

Does MongoDB have anything like Row-Level Security? Not natively in the same form. Its authorization story is role-based access control at the database/collection level, or — if you're using a BaaS layer like MongoDB Atlas App Services — a separate rules language evaluated per request. Neither attaches a policy directly to a document the way Postgres RLS attaches to a row, evaluated on every connection including ones you didn't write.

Is this comparison different for an AI-heavy application? Somewhat. Postgres's pgvector extension does similarity search on embeddings inside the same database as your relational data, which avoids a second system for vector search. MongoDB added native vector search too. Neither difference changes the argument above — it's still about which parts of your data have real relationships and which don't.

Templates that already have a storefront to put behind it

ASoc Compound is an investment-platform landing page, and investment data is about as relational as it gets — positions, accounts, transactions, all needing integrity guarantees a document store makes you build yourself. ASoc Axiom is an AI-services landing page, the category where the pgvector-vs-native-vector-search question above actually comes up. ASoc Quest is a game-store landing page — a catalog-fronting product, which is the reminder that even a MongoDB-shaped dataset usually sits behind a page that's just static content, not a live query.

Browse the full set of Next.js landing page templates, or the Tailwind landing page templates. For the commerce layer this post assumes but doesn't re-explain, see Supabase vs Firebase for a template-based SaaS.

Keep reading

Comparison10 min read

Supabase vs Neon: Both Are Postgres, So the Database Isn't the Call

Six RLS policies, all built on auth.uid() — a function Postgres does not have. An audit of exactly which of our own code depends on the platform rather than the database.

Read more
Comparison9 min read

Supabase vs PlanetScale: 7 Foreign Keys Into a Table MySQL Doesn't Have

The dialect differences port. The 7 foreign keys into auth.users and 6 policies built on auth.uid() don't — inventoried line by line from this storefront's 8 migrations.

Read more
Comparison9 min read

Supabase vs Prisma: An ORM Cannot See Your Row-Level Security

Point Prisma at a Supabase database and your policies stop working — in one of two directions, neither the one you wrote. Our own six policies, audited against that.

Read more