What Databricks actually buys you isn't a better Silver, it's a defensible one

Everyone sells the medallion architecture on the same promise: your Silver layer is cleaner than your Bronze. Deduplicated, conformed, typed, joined. That's true, and it's also not the payoff that matters. After building a governed memory store on Databricks — Delta for the raw tier, Postgres for the serving tier — I've landed on a sharper way to say what you're actually buying. It isn't a better Silver. It's a defensible one: a layer you can prove wasn't tampered with, that won't leak one client's existence to another, and where a cross-tenant write is a bug you literally cannot commit. Let me show you the difference, including the part where I was wrong.
The immutability I claimed but didn't have
I built a Bronze tier and called it immutable, because it was append-only. On Delta that's one property:
ALTER TABLE bronze.events SET TBLPROPERTIES ('delta.appendOnly' = 'true');
-- now an UPDATE, DELETE, or MERGE...WHEN MATCHED fails:
-- [DELTA_CANNOT_MODIFY_APPEND_ONLY]
On Postgres it's a BEFORE UPDATE OR DELETE trigger that raises an exception. Same idea, same good feeling: the raw archive can't be mutated. I shipped it. Then I ran the one test I should have run first — can the person the guardrail is meant to constrain turn it off?
ALTER TABLE bronze.events SET TBLPROPERTIES ('delta.appendOnly' = 'false');
DELETE FROM bronze.events WHERE embarrassing = true;
ALTER TABLE bronze.events SET TBLPROPERTIES ('delta.appendOnly' = 'true');
The delete succeeded. Of course it did. Anyone with ALTER can disarm the property, mutate the table, and re-arm it. The Postgres trigger has the identical hole — the table owner just runs ALTER TABLE ... DISABLE TRIGGER ALL and walks right in. I'll admit it plainly: I had this exact hole in shipped code, because the application's own write role owned the table. The guardrail was self-disarming, and the principal I was "protecting against" was the app itself.
Here's the lesson worth more than the code: append-only enforcement is a guardrail, not a control. A guardrail keeps you from falling off by accident. A control stops someone who's trying. Don't confuse the two — I did.
What actually prevents it: split the ownership
Prevention doesn't come from a smarter trigger. It comes from making sure the writer doesn't own the table. Separate the role that runs migrations and owns the schema from the runtime role that only inserts:
-- owner role runs DDL/migrations and owns bronze.events
-- runtime role can insert, and nothing else
REVOKE UPDATE, DELETE ON bronze.events FROM app_writer;
-- app_writer now has no ALTER, so it can't disable its own immutability
Now the app can append and cannot mutate — not because a trigger says no, but because the engine never granted it the power to say yes. Prevention comes from grants, not from a guardrail the writer can disarm. Treat append-only as defense-in-depth and tamper-detection. Never as the boundary.
Delta's real edge: it records the cover-up
So append-only can be turned off on any engine. Does Delta buy you anything here? Yes — and it's the single most underrated thing in the format. When someone disarms, deletes, and re-arms, every one of those steps is a numbered commit in the transaction log. Walk the history of a tampered table and you'll see the whole confession, something like:
version 4 UNSET TBLPROPERTIES (delta.appendOnly)
version 5 SET TBLPROPERTIES (delta.appendOnly = false)
version 6 DELETE
version 7 SET TBLPROPERTIES (delta.appendOnly = true)
The attacker put the property back exactly the way they found it. The table looks pristine. The log says otherwise, permanently. That's tamper-evidence — Delta doesn't stop the tampering, it makes the tampering undeniable. Plain Postgres gives you nothing equivalent without wiring up an audit trigger or logical decoding yourself. So the honest framing is: append-only is the guardrail, ownership is the control, and on Delta the transaction log is the auditor who never blinks.

The leak nobody checks: your catalog is telling on you
Now the tier that actually serves queries. If you isolate tenants with a schema each in a shared Postgres database, you've solved the data leak — client A's role can't read client B's rows. You probably haven't solved the metadata leak. Give client A's role zero data access to client B's schema, then run this as that role:
SELECT tablename FROM pg_tables WHERE schemaname = 'client_b';
-- ('memory')
Client A just learned that client B exists, and what their tables are called. pg_catalog is world-readable to anyone who can connect. For a hobby project, nbd (no big deal). For a multi-tenant design, that's a disclosure problem — the tenant roster is supposed to be confidential, and the database is handing it out.
This is where Unity Catalog earns its keep, and it's not the reason vendors usually pitch it. UC filters metadata by grant. A principal granted one catalog sees only that catalog in SHOW CATALOGS and in information_schema — the others don't exist as far as it can tell. Two rules make it hold:
- Grant
USE CATALOGper tenant, and only per tenant. - Don't hand out
BROWSEbroadly. It's a real Unity Catalog privilege that exposes an object's metadata — names, comments, tags — with no data access, and Databricks actually recommends granting it on catalogs to theAll account usersgroup so data is discoverable org-wide. For a single company that's a feature; for tenant isolation it's the one grant that re-opens the leak you just closed. Scope discovery per tenant instead.
One caveat so you don't over-trust it: metadata filtering lives on the UC governance path. If a tenant reaches the underlying storage or a UC-registered Postgres by a direct connection that skips the metastore, UC isn't in that path and can't filter anything. Governance you can bypass is decoration. Know where your control plane actually sits.
Isolation belongs on the connection, not on a column
The last piece is the one I feel most strongly about. The tempting way to keep tenants apart in a shared table is a namespace column that the application sets on every write. Don't. A wrong namespace value looks exactly like a right one. Nothing rejects it, nothing logs it as suspicious, and the contamination is silent and undetectable after the fact — you find out when client A sees a fact that belongs to client B, and by then it's in the Silver layer.
Push the guarantee down to where code can't be wrong. If the worker connects as the tenant's own role to the tenant's own database, a cross-tenant write isn't a value you have to get right — it's an operation the engine refuses. You've turned a mistake anyone could make into a bug that cannot be written. The invariant to hold onto: no code path ever holds a connection that can see two tenants at once. Demote namespace to what it's good for — scoping within a tenant (owner, project), where a slip is embarrassing rather than a breach.
So what did Databricks actually buy you?
Not a cleaner Silver. A defensible one. Delta's transaction log means you can prove the raw tier wasn't quietly rewritten. Unity Catalog's grant-filtered metadata means one client can't enumerate another. And the database engine — Databricks or Postgres — buys you connection-level isolation that turns a whole class of contamination into an impossibility instead of a code review. The dedup and the typing are table stakes; every tool does that. Defensibility is the part you can put in front of a security review and a skeptical client, and that's the part that's worth paying for.
If you're running a multi-tenant medallion setup, go check one thing today: can the role that writes your Bronze also ALTER it? If yes, you have my old bug. And if you've found a cleaner pattern for any of this — especially metadata isolation on plain Postgres — I'd genuinely love to hear it. As always, I'm here to help!