A Database for Every Customer

Senior Engineering Manager, Application Platform·2026·12 months·24 people·6 min read

Every paying customer on its own encrypted database, 1,200+ isolated in production, giving commercial and regulated customers real data separation and control.

Overview

Led the platform-wide move from a shared multi-tenant database to a database-per-customer architecture, adding per-tenant encryption keys and a routing layer while migrating live customers with no data loss. Access-control hardening ran in parallel to satisfy enterprise and federal compliance requirements.

Problem

The product ran on a shared multi-tenant database. Enterprise and federal customers increasingly required their data to be physically separated and encrypted under their own keys, and some of the largest deals were contingent on it. A shared database with logical filters couldn't satisfy those requirements, and the largest customers held datasets big enough that migrating them safely was a real engineering problem in its own right.

Constraints

  • Migrations ran on live customers and had to complete with zero data loss and no meaningful downtime.
  • Every query in the product had to route to the correct customer database without rewriting application logic everywhere.
  • The largest customers held datasets large enough that a naive migration would take far too long.
  • Each customer's data had to be encrypted under its own key, coordinated with customer success on timing per account.

Approach

Moved to a database-per-tenant model: each paying customer on its own PostgreSQL database, encrypted under a per-tenant key that we managed in AWS KMS, with a DynamoDB-backed tenant router directing every query to the right place. Migrated customers in phases, hardest and highest-value first, coordinating each cutover with customer success. Paired the isolation work with a shift from broad internal admin access to time-bound, logged escalation, so the separation held up as a compliance story and not just an architecture.

Key Decisions

Give each customer a physically separate database rather than isolating within a shared one

Reasoning:

Enterprise and federal compliance required real data separation and per-tenant encryption keys. Row-level or schema-level isolation inside a shared database can't offer a customer its own encryption key or a clean physical boundary, which is exactly what these deals were asking for.

Alternatives considered:
  • Schema-per-tenant within a shared database.
  • Row-level security and logical filtering in the shared database.

Encrypt each tenant under its own key, managed by us in KMS

Reasoning:

Per-tenant keys give each customer cryptographic separation and contain the blast radius of any single key. It's also the concrete control enterprise and federal security reviews look for, so it did double duty as architecture and as a compliance answer. Holding the keys ourselves per tenant was also the foundation for later handing that control to customers directly.

Alternatives considered:
  • A single shared encryption key across all tenant databases.

Route every query through a DynamoDB-backed tenant router

Reasoning:

A central routing layer meant the rest of the application didn't need per-request database wiring scattered through it. One place resolved which database a request belonged to, which kept the migration tractable across a large codebase.

Alternatives considered:
  • Resolve the target database inline in application code on every request.

Migrate the largest, highest-risk customers first

Reasoning:

The hardest migrations were the biggest datasets and the most important accounts. Proving those early, rather than saving them for last, surfaced the real problems while there was still room to solve them and built confidence for the rest.

Alternatives considered:
  • Migrate smallest and lowest-risk customers first to build momentum.

Tech Stack

  • PostgreSQL
  • AWS KMS
  • DynamoDB
  • Python
  • AWS

Result & Impact

  • 1,200+ in US production
    Isolated tenant databases
  • 5,900+
    Customer accounts routed to their own database
  • ~37 million records per hour
    Migration throughput
  • hours, not days, for the biggest enterprise datasets
    Largest-customer migration time
  • shipped; EU region effectively complete
    Status

This one shipped. Every paying customer runs on its own encrypted database, the largest enterprise and healthcare-scale accounts migrated in hours, and the EU region was effectively complete. The isolation directly unblocked enterprise and federal deals that required data separation, and the paired shift to time-bound, logged internal access turned a piece of architecture into a compliance posture the security reviews those deals depended on could actually accept.

Learnings

  • Data separation for enterprise and federal isn't a logical-filtering problem; when customers need their own encryption key and a real boundary, database-per-tenant is the honest answer.
  • A central routing layer is what makes a migration of this size survivable across a large codebase; without it, the change touches everything.
  • Isolation is only half the compliance story; pairing it with time-bound, logged admin access is what let it stand up to security review.

Why Separate Databases

The product had grown up on a shared multi-tenant database, and that stopped being acceptable as the customer base moved upmarket. It wasn’t only a regulatory story: commercial customers increasingly wanted tighter control over their own data regardless of any mandate, while enterprise and federal buyers needed it physically separated and encrypted under their own keys, with some of the largest deals contingent on it. Logical isolation inside a shared database can filter rows, but it can’t hand a customer its own encryption key or a clean physical boundary. The requirement was real separation, so the architecture had to give each customer its own database.

Routing and Keys

Two decisions carried the design. Each paying customer got its own PostgreSQL database encrypted under a per-tenant key that we managed in KMS, so separation was cryptographic and not just organizational. A DynamoDB-backed tenant router sat in front of everything and resolved which database each request belonged to, which meant the rest of the application didn’t have to be rewritten request by request. That routing layer was what kept a change this broad from touching every corner of the codebase at once.

Migrating Live Customers

Migrations ran on live customers, so zero data loss and negligible downtime were the baseline, not the goal. The hardest cases came first: the biggest datasets and the most important accounts, migrated in hours at a throughput of tens of millions of records an hour, each cutover timed with customer success. Doing the largest enterprise and healthcare-scale accounts early surfaced the genuinely difficult problems while there was still room to fix them, and made the long tail of smaller migrations routine.

Isolation as a Compliance Posture

Separating the data was only half of what the deals actually required. Alongside the isolation, internal access moved from broad standing admin rights to time-bound, logged escalation, so any employee touching a customer’s account did so through an explicit, expiring grant that showed up in the record. That pairing is what turned a database architecture into something a federal or enterprise security review could sign off on. By the end, the work was shipped: every paying customer on its own encrypted database, the EU region effectively done, and the deals that hinged on data separation cleared to move.

What It Opened Up

The isolation opened doors for future work. With every customer on its own database and its own key, customer-managed keys became a natural next step, in place of the keys we held, and further out, bring-your-own-database or bring-your-own-SaaS for customers who wanted to run on their own infrastructure. That was the direction the architecture was built to make possible.