Skip to main content
Back to BlogEngineering

Why Your 'Bulletproof' Database Architecture Will Fail: A Senior Engineer Debate

Two senior engineers tear apart a 'bulletproof' multi-tenant PostgreSQL schema. Raw dialogue, brutal honesty, and the engineering truths that emerged. This is what real code reviews should look like.

Engineering Team
January 17, 2026
18 min read

What follows is a real technical debate between two senior engineers. One walked into a design review confident his schema was perfect. He walked out with a new understanding of what "bulletproof" actually means.

We're publishing this because most engineering blogs sanitize the conflict. They show you the "after" without the messy "during." But real growth happens in the friction. Real learning happens when someone tells you your code is broken and forces you to defend it.

This is that conversation. Unfiltered. With the lessons we both learned.


The Opening Conflict: When "Bulletproof" Meets Reality

It started with a single word. I had spent weeks designing a multi-tenant SaaS database schema. PostgreSQL, Row Level Security, clean separation of concerns. I presented it to the team and made one critical mistake: I called it "bulletproof."

The response was immediate and brutal:

The Critic
"This schema you've slapped together and called 'BULLETPROOF' is anything but. It's a bloated, over-engineered mess that's begging for security breaches, performance craters, and maintenance nightmares. It's like building a fortress with paper walls—looks impressive on the blueprint, but crumbles under real pressure."

I wanted to push back. But then came the counter-argument:

The Response
"Calling a PostgreSQL multi-tenant schema 'bulletproof' is unjustified unless isolation, privilege boundaries, and performance characteristics are enforced by the engine and continuously falsified in CI."

That hit differently. Not an attack on my competence—a precise statement about what the word actually means in engineering.

Pro Tip: Lesson learned: The word "bulletproof" triggered the entire conflict because it implies proven invariants, not design intent. If you can't prove it in CI, don't claim it.

The Multi-Tenancy War: Shared Tables vs. True Isolation

The first major battleground was multi-tenancy. I had chosen the "standard" SaaS pattern: shared database, shared schema, Row Level Security to keep tenants apart.

The attack was surgical:

The Critic
"Your shared-database, shared-schema approach is a classic rookie mistake. This isn't bulletproof; it's a ticking bomb for data breaches and downtime. A bug in your RLS helpers could expose one agency's contracts to another's clients."

"Rookie mistake" stung. But was it wrong? I pushed back:

My Defense
"Shared-table tenancy is not rookie trash; it's the dominant SaaS pattern—but only when tenant keys are enforced at the engine level. Without composite FKs or partitioning, shared tenancy is indeed unsafe."

The escalation continued:

The Critic (Escalating)
"Hybrid (shared for common, per-schema for sensitive) is often better, but you didn't even consider it."

He was right. I hadn't. I had chosen the default pattern without analyzing alternatives.

The Truth We Both Agreed On

After the heat subsided, we reached consensus: Shared tables are only safe if tenant lineage is enforced by foreign keys, not by application logic or RLS alone.

What does that mean practically?

  • Every child table must carry tenant_id and validate it against its parent via composite foreign keys
  • Composite primary keys should include tenant_id to physically cluster tenant data
  • Partitioning by tenant isn't optional at scale—it's mandatory for both security and performance
  • RLS is a backup, not the primary isolation mechanism

My opinion: The "shared everything" approach works for MVPs, but the moment you have paying customers, you need defense-in-depth. If a single RLS policy bug can leak data, your architecture isn't production-ready.


The SECURITY DEFINER Trap: When Helper Functions Become Backdoors

I was proud of my helper functions. To improve developer experience, I had wrapped complex logic in SECURITY DEFINER functions with elevated privileges.

This is where the conversation got intense:

The Critic
"SECURITY DEFINER functions are a privilege escalation playground. If an attacker creates a malicious object in a writable schema, they can hijack your search_path and run code as the function owner. Your CI regex is cute but worthless without revoking CREATE on public."

"Cute but worthless." I felt that one. But he wasn't wrong. I had checks, but they were incomplete:

My Acknowledgment
"Search_path locking is mandatory, and ownership must be verified in CI—agreed. Security is not 'having functions', it's proving who owns them and who can execute them."

What I Was Missing

Here's what I didn't fully understand: In PostgreSQL, a SECURITY DEFINER function runs with the privileges of its owner, not the caller. If that function references an unqualified object name and the search_path isn't locked down, an attacker can:

  • Create a malicious table or function in the public schema
  • Name it the same as something your function calls
  • Watch your function execute their code with elevated privileges

The fix is straightforward but non-negotiable:

  • Lock the search_path explicitly in every SECURITY DEFINER function: SET search_path = my_schema, pg_catalog
  • Revoke CREATE on public for all roles except superusers
  • CI must verify ownership—reject any SECURITY DEFINER function not owned by a trusted role
  • Audit execution paths—know exactly what each function can call
Pro Tip: SECURITY DEFINER requires ownership + search_path lockdown + schema isolation + CI verification. Without all four, it's worse than using RLS alone because it gives attackers a privilege escalation vector.

RLS Performance: The Silent Killer

I had leaned heavily on Row Level Security. It's elegant. It's built into PostgreSQL. It "just works."

Except when it doesn't:

The Critic
"Your policies are performance killers—evaluated per row. Non-leakproof functions block index usage and force seq scans. RLS is not for complex ABAC like yours."

This was the moment I realized I had never actually profiled my RLS policies:

My Acceptance
"Correct—RLS must be planner-friendly or it collapses under scale. Policies must be measurable: if EXPLAIN shows Seq Scan, CI must fail."

The Ugly Truth About RLS Performance

Here's what the PostgreSQL documentation won't tell you in bold letters: RLS policies are evaluated per row during query execution. If your policy calls a function, that function runs for every row scanned.

Worse, if that function isn't marked LEAKPROOF, the query planner can't push predicates down or use indexes efficiently. You end up with sequential scans on million-row tables.

What we agreed RLS requires:

  • Simple, inline predicates—avoid function calls in policies when possible
  • LEAKPROOF functions only—if you must use functions, they must be marked appropriately
  • EXPLAIN in CI—if any RLS-protected query shows a sequential scan on a large table, the build fails
  • Consider alternatives—sometimes a view with explicit WHERE clauses outperforms RLS

My take: RLS is beautiful for authorization, but it's not "free." Every policy is code that runs at query time. Treat it with the same performance scrutiny as any other hot path.


Immutability Without Lifecycle: A Storage Bomb

I had designed several tables as append-only for audit purposes. "Immutable data is secure data," I reasoned.

The Critic
"Immutable data balloons storage; no partitioning means your DB swells forever. At scale, this trashes performance."

Short, brutal, correct:

My Agreement
"Immutability without lifecycle is unfinished design. Partitioning + retention is mandatory for append-only tables."

This wasn't a flaw in the concept of immutability—it was a flaw in my implementation. Append-only tables need:

  • Time-based partitioning (monthly, quarterly) for efficient archival
  • Retention policies—what gets archived? What gets deleted? When?
  • Partition pruning—queries should automatically skip old partitions
  • Cold storage migration—old partitions move to cheaper storage

The JSONB Trap: Flexibility vs. Query Planning

I love JSONB. Schemaless flexibility, GIN indexes, containment operators. What's not to love?

The Critic
"JSONB everywhere is lazy. Planner guesses selectivity and makes garbage plans."

"Lazy." Another one that hurt. But:

My Concession
"JSONB is fine for payloads, not for query drivers. Generated columns and partial indexes are required."

The problem with JSONB in WHERE clauses is that PostgreSQL can't accurately estimate selectivity. It makes assumptions that are often wildly wrong, leading to poor execution plans.

The rule we landed on:

  • JSONB is for storage—perfect for flexible payloads, metadata, configurations
  • Extract query-critical fields—use generated columns or materialized fields for anything in a WHERE clause
  • Index the extracted fields, not the JSONB paths
  • Test with realistic data volumes—JSONB queries that work on 1000 rows collapse at 1M rows

The Convergence: What "Bulletproof" Actually Means

After hours of this, something shifted. The conflict became collaboration. I made a commitment:

My Commitment
"I'll deliver a tight, executable pack with zero syntax errors, lock-safe migrations, and falsifiable tests."

The response was the first positive thing I'd heard all day:

The Response
"Now we are finally doing engineering instead of arguing."

That sentence captured everything. We weren't enemies. We were engineers trying to build something that wouldn't break under pressure. The conflict was the process.


The Shared Truth: A New Definition of "Bulletproof"

Here's what we agreed on. This is the doctrine that emerged from the debate:

Key Takeaway
Bulletproof is not a design style. It is a property proven by constraints, ownership, query plans, and tests. If CI cannot falsify it, it is not real. Specifically:
  • Tenant isolation must be enforced by foreign keys and partitioning, not just RLS
  • SECURITY DEFINER requires explicit search_path locking, ownership verification, and schema lockdown
  • RLS policies must be query-plan tested—Seq Scan on production tables is a build failure
  • Immutability requires a complete lifecycle: partitioning, retention, archival
  • JSONB is for storage, not filtering—extract fields for queries
  • Everything must be provable in CI, or it's just hope

What Changed in Our Architecture

For those who want the concrete outcomes, here's what we implemented after this debate:

1. Multi-Tenancy

  • Composite foreign keys with tenant_id on every child table
  • Range partitioning by tenant_id for high-volume tables
  • Hybrid approach: shared tables for config, partitioned for data

2. Security

  • All SECURITY DEFINER functions now have explicit SET search_path
  • REVOKE CREATE ON SCHEMA public FROM PUBLIC is in our bootstrap migration
  • CI checks function ownership and rejects unsigned definitions

3. Performance

  • RLS policies are inline predicates only—no function calls
  • CI runs EXPLAIN on critical queries and fails on Seq Scan
  • JSONB fields used in queries are extracted to generated columns

4. Data Lifecycle

  • All append-only tables are partitioned by month
  • Automated partition creation and archival via pg_partman
  • Retention policies defined and enforced

Final Thoughts: Conflict as Process

This conversation could have gone differently. I could have dismissed the criticism as aggressive. The critic could have stopped at "rookie mistake" and walked away feeling superior.

Instead, we both committed to finding the truth. And the truth wasn't that one of us was right—it was that real security and performance require proof, not claims.

If you're designing a multi-tenant database, I hope this dialogue saves you the painful lessons we learned. And if you're doing code reviews, I hope it reminds you: the goal isn't to win the argument. It's to build something that won't break.

Bulletproof isn't a word you say. It's a property you prove.


This post is based on real engineering discussions. Some sentences have been preserved verbatim to maintain authenticity. If you have questions about implementing any of these patterns, reach out to our engineering team.

Get Started

Make AI Your Edge.

Book a free AI assessment. We'll show you exactly which tools will save time, cut costs, and grow revenue — in weeks, not months.

Free 30-minute call. No commitment required.