Why we pay more for per-tenant SQL Server isolation
The cheapest way to build multi-tenant SaaS is one database, every tenant in the same tables, a tenant_id column, and discipline. PulseCargo doesn't do that. Here's why we made the more expensive call — and why it pays back the first time a SOC 2 reviewer walks in the door.
If you read enough multi-tenant SaaS architecture posts you'll notice they all open the same way: row-level security is fine if your engineers are disciplined; per-tenant databases are over-engineering until you have a compliance reason. The implication is that you should default to row-level security and graduate to isolation when an enterprise procurement team forces you to.
That framing assumes the cost of switching later is small. In freight forwarding it isn't. The procurement teams that make per-tenant isolation matter — SOC 2 Type II reviews, ISO 27001 audits, Fortune 500 vendor security questionnaires — arrive on day one of the conversation, not in year three. If your answer to "how do you separate tenant data" is multi-paragraph and conditional, you've already lost the meeting.
So we made the more expensive choice up front.
What PulseCargo actually does
Every tenant gets their own SQL Server database. The naming is mundane:
PulseCargoDb— the portal database, holding tenant identities, billing plans, service agreements, portal admin accounts, and the cross-tenant infrastructure.PulseCargo__<tenant-slug>— one per customer tenant, holding all of that tenant's customer data: shipments, orders, containers, customs entries, invoices, documents, audit logs, AI query logs.
The web tier, API tier, and background workers receive a tenant identifier on every request via TenantResolutionMiddleware. The middleware looks up the tenant slug, computes the connection string for that tenant's database, and binds that connection to the request scope. Every database call within the request — whether through Entity Framework Core, Dapper, or direct ADO — uses that bound connection.
A query that doesn't have a tenant scope can't connect to a tenant database. There's no shared table to forget the WHERE clause on. The forgotten-WHERE-clause failure mode — the single most common cause of cross-tenant data leaks in shared-schema SaaS — doesn't exist in our codebase.
The trade-offs we accept
This is more expensive than row-level isolation in four specific ways. Each is real, and each is one we'd make again.
Higher per-customer storage cost. Each tenant database has its own indexes, transaction logs, statistics, and overhead. The marginal cost of a small tenant is higher than it would be in a shared-schema model. We pay for that. It scales linearly. The cost of a cross-tenant data leak doesn't scale linearly — it scales with how badly the vendor wants to stay in business.
Cross-tenant analytics is more work. The aggregated industry benchmarks we offer as an opt-in feature at the Professional tier require iterating participating tenants, computing per-tenant statistics, and writing the aggregate to a dedicated benchmark store. Most shared-schema vendors get cheap aggregates for free. We pay this cost as a once-daily background job. The benefit is that the aggregation is k-anonymous (k ≥ 5) by construction, and tenants opt in via an explicit flag rather than discovering their data was already in the pile.
Provisioning takes more steps. A new tenant requires database creation, schema migration, starter-pack import, and EF migration history backfill. We have a script (scripts/backfill-tenant-ef-history.ps1) and a provisioning pipeline that automate this; it's roughly a two-minute step. Not free, but not painful.
Schema migrations apply per database. A schema change has to be applied across every tenant database, not just one. EF Core migrations are applied at deploy time per tenant, with rollback paths. The discipline cost is real but the deployment pipeline handles it transparently.
Total operational cost: somewhere between 1.5x and 2x what shared-schema would cost us at our current tenant count, asymptotically converging on roughly 1.2x as tenant count grows.
The payback — what isolation buys you
Some of what isolation buys is obvious. Some of it isn't.
The forgotten-WHERE-clause class of bug doesn't exist. A new query, a refactor, a stored procedure migration, a junior engineer's first PR — none of them can leak data across tenants because there's no shared table to forget the predicate on. ORM filter bypass via raw SQL? Same answer. Cache key reuse? Same answer.
Background jobs can't pull "all rows" without specifying a tenant. The job has to ask which tenant it's running for in order to get a connection string. The default is "no connection."
BI tools connecting directly are tenant-bounded by credential. Power BI Embedded uses a per-tenant workspace credential. Even if a BI consultant tries to do something exotic, the credential they have access to is tenant-scoped at the database layer.
Backups are tenant-scoped by default. Each tenant has its own backup file. Restoring to the wrong place isn't a "be careful" problem; it's a "won't connect" problem.
The compliance audit answer is one sentence. "How do you separate tenant data?" "Each tenant has their own SQL Server database, resolved by middleware. The connection string for one tenant cannot reach another tenant's database." That's the whole answer. Compliance auditors notice it within five minutes.
The audit we ran on ourselves
On 2026-04-24 we ran a comprehensive tenant-isolation audit on the codebase. 61 controllers, approximately 140 endpoints, every database query path. We were specifically looking for cross-tenant data exposure: places where a tenant could see another tenant's data either through API misbehavior, through query injection, or through shared-state bugs.
Zero CRITICAL findings.
One INFO-level finding identified: outbound integration audit logging not yet implemented for all third-party services (Stripe Connect, TMS provider probes, AI providers). This is a SOC 2 CC4.1 / CC7.2 evidence gap, not a cross-tenant exposure. The pattern is in place for eAdaptor; remaining services are in the build queue. We surface this gap honestly in our security documentation rather than discovering it during a SOC 2 evidence review.
The full audit report is shareable with security teams on request. (Email security@pulsecargo.ai.)
What this is not
Per-tenant database isolation isn't a substitute for the rest of the security posture. It's the substrate. AES-256 encryption at rest, TLS 1.3 in transit, native MFA + SSO, RBAC, audit logging on every action, GDPR / CCPA self-service endpoints, software escrow with tested rehydration, leading compliance frameworks tracked — all of those still have to ship and have to be done right. Isolation just means the floor under all of that doesn't have a hole in it.
It also isn't a brag. There are credible reasons to ship shared-schema multi-tenancy — horizontal scale, cost discipline, simpler ops — and many great SaaS products run that way. We chose differently because the buyer in our market notices, the cost differential is bounded, and the worst-case downside of getting it wrong is unbounded.
If you're a freight forwarder evaluating PulseCargo, ask the question. Ask it of every vendor in your evaluation. Whoever can answer it in one sentence is probably the safer vendor.
Want the deeper version? The Per-Tenant Database Isolation Architecture white paper expands every point in this post for security teams and procurement reviewers. The full Tenant Isolation Audit report is shareable on request — email security@pulsecargo.ai.
This is the first post in the "Inside PulseCargo" technical content series. We'll publish one of these per month, walking through the engineering decisions that shape the product. Next up: how the Synthetic Intelligence retrieval layer keeps tenant context separated when the underlying language model is shared.
Want a 30-minute briefing?
If your security or procurement team has questions this post doesn't answer, let's set up a call.
Request a Demo →