Data Nexus

Data · We run it ourselves

Supabase

Managed PostgreSQL with an authentication service, file storage and a generated HTTP API in front of it, addressable directly from a browser.

This is not an entry about the database. PostgreSQL is covered on its own page and we run it unmanaged elsewhere; what Supabase sells is the apparatus around it — hosting, a token issuer, object storage, and an API key that ships inside the client. The whole arrangement rests on one Postgres feature doing its job, which is why row-level security is not a hardening step here but the load-bearing wall.

supabase.com

01/Why this one

  1. 01

    What is being bought is operational, not relational. Postgres itself costs nothing; connection pooling, point-in-time recovery, a restore somebody else has actually rehearsed and a JWT issuer we did not write are the line items. On a product with eleven tables and one paying tenant, building that apparatus costs more than the product it carries, and that is the trade this entry is about.


  2. 02

    The publishable key is compiled into the client and visible to anyone who opens the network tab. That is the design rather than a leak: the key names the project and nothing else, and every question of who may read which row is answered by a policy attached to the table. It inverts the usual arrangement — instead of a server trusted because it is the only thing holding a secret, there is a database that trusts nothing and re-checks every statement.


  3. 03

    The pattern that makes it useful is a write-only table. A lead capture table enables row-level security, permits an anonymous INSERT with a check of true, and refuses SELECT with a policy of false. The form then works from a page with no server behind it, and anyone replaying the same key against the same endpoint gets an empty set rather than the list of names. The rule is a dozen lines of SQL sitting next to the data, so a second application built against that table later cannot widen it by forgetting.


  4. 04

    Row-level security is a PostgreSQL feature and not a Supabase one, which is precisely what keeps the exit open. The schema, the policies and the migration history are ordinary SQL and move to any Postgres, managed or not. Auth and storage do not move, and naming which half is portable before the first table is written is the difference between a platform decision and a trap.


  5. 05

    Session handling, password reset, OAuth callbacks and refresh rotation are solved problems with a long tail of ways to be subtly wrong. The part that earns the dependency is that the issued token arrives inside the database as auth.uid(), so the identity that signed in is the identity the row policy tests. Identity is not re-derived in a middle tier where two services can come to different conclusions about the same user.


  6. 06

    Storage objects are rows in a table governed by the same policy language, so a private bucket is expressed the way a private table is expressed. A second access-control system with its own vocabulary is a second place for the rule to be right in one and wrong in the other.


  7. 07

    Schema is kept as files under version control rather than as a state somebody clicked into the dashboard — a migrations directory in one project, one .sql file per table in another. A schema whose history exists only in a web console cannot be reviewed in a diff, replayed onto a fresh project, or rolled back after a bad afternoon.

02/What you can check

An entry that cannot point at something you can open, run or read does not compile. That is a property of the type, not a promise in a paragraph.

Case record
The editorial content, the lead records and the search-console history the client's own team works with daily.Read the record

In our code
DYD-main/supabase/contact_leads.sql — The write-only table in full: row-level security enabled, an anonymous INSERT policy with a check of true, and a SELECT policy of false, so the browser can submit a lead and nobody can read the table back with the same key.

In our code
DYD-main/supabase/analytics.sql and respond_webhooks.sql — Page views, events and inbound conversation webhooks landing in the same project under the same insert-only policies, rather than in a third-party analytics account.

In our code
status-kvo-app/supabase/migrations — Schema history as versioned files, so the database has a past that replays onto an empty project.

In our code
carfleet-main/packages/db and carfleet-main/supabase/config.toml — The database package and project configuration behind a product we run ourselves, with a second repository, carfleet-rel, on the same arrangement.

Product we run
A vertical operating system for car rental running on it, whose uptime is ours rather than a client's.Carfleet

Property of the thing
Row-level security is core PostgreSQL, available since version 9.5 and unrelated to any vendor: ALTER TABLE ... ENABLE ROW LEVEL SECURITY and CREATE POLICY are plain SQL statements. — Run both against any local Postgres 9.5 or later and inspect the table with \d — the policies are reported by the server itself, with nothing from Supabase installed.

Property of the thing
Row-level security is off by default. A table created in the dashboard is exposed through the generated API immediately, and until a policy exists the anonymous key reads it in full. — Create a table in any project without enabling it and open the Security Advisor: the project reports the table as exposed, which is the vendor conceding the default.
03/Where it stops

Invariant

A policy is a predicate the planner has to satisfy for every row it touches, so a rule that joins to a membership table or calls a function per row turns an index scan into a per-row cost — and the query stays correct while getting slower, which is the hardest class of regression to attribute, because nothing ever returns a permission error to point at. Past a few thousand tenants with nested policies the honest move is to stop expressing the rule in row-level security and put it behind a service that owns the query. The exit is also only half open: schema, policies and migrations are ordinary Postgres and move anywhere, but auth and storage are the vendor's, so leaving costs whatever was built on those two.