The data layer: RDS, pgvector, secrets, encryption

One Postgres for both your relational data and your embeddings. Secrets in Secrets Manager. Encryption on by default. The MVP-grade data layer that doesn't need a forklift later.

The data layer: RDS, pgvector, secrets, encryption

Here's the layman version. Your AI product has two kinds of memory. One is the boring kind, users, accounts, billing records, the consultant's playbook, the audit log. Rows and columns. The other is the AI-flavored kind, the chunks of the consultant's body of work that get pulled into the model's context when it answers a question. Those chunks live as embeddings (a list of numbers that represents the meaning of a chunk of text, if you want to look it up later). Most teams set up two different databases for these two kinds of memory. One database for the boring stuff, a separate "vector database" for the AI stuff. Two systems. Two failure modes. Two bills.

You don't need two databases. One Postgres can do both. That's the whole shape of this article.

One store: Postgres for data + embeddings RDS Postgres + pgvector Relational tables tenants users sessions queries approvals audit_log pgvector embeddings secret_sauce_chunks customer_history golden_examples prompts_indexed docs_indexed queries_indexed Secrets Manager + KMS , encryption keys, API tokens, model creds KMS keys per tenant. Encryption at rest by default. One database. Two access patterns. Aurora can wait.
One store data + embeddings

The technical version: RDS Postgres with the pgvector extension, encryption on by default, secrets in Secrets Manager with KMS-managed keys. Aurora can wait. Specialized vector DBs can wait. You can run real workloads on this shape for a long time before you need anything fancier, and when you do need something fancier, you're moving from a solid spot, not from a hack.

Why one store

When teams reach for a separate vector database (Pinecone, Weaviate, Qdrant, Chroma, whichever) they're solving a problem most MVPs don't actually have yet. They're solving for tens of millions of vectors and ultra-low-latency similarity search at high throughput. If you have that problem, congratulations, you also have revenue, and you can pay for the dedicated thing. You probably don't have that problem yet.

The problem you do have is operational complexity. Every additional system in your stack is another thing that can fail, another set of credentials, another backup story, another upgrade path, another set of metrics you have to learn to read. For an MVP, every system you don't add is a free hour you get to spend on the actual product.

pgvector (a Postgres extension that adds vector data types and similarity search operators, it's been production-grade since 2023) gives you the AI-flavored memory inside the same database that holds your users table. The queries look like ordinary SQL with an extra distance operator. The backups are your normal Postgres backups. The replication is your normal Postgres replication. The credentials are the same credentials. The audit log is the same audit log.

When you join an embedding lookup to a tenant filter to a date range, you write one query. When you do it across two systems, you write glue code, you reconcile failures, you handle partial writes, and you accept that the two stores will get out of sync at some point. The one-store version is just better for the size you're at.

The shape I actually run

The MVP-grade data layer is small. Three pieces.

RDS Postgres, single instance, smallest sensible size. I usually start with db.t4g.medium for a real product or db.t4g.small for a prototype. Multi-AZ off until I have customers who'd notice an outage. Storage on gp3, starting at 50 GB with auto-scaling enabled so I don't get paged at 2am because some import job wrote more than I expected.

pgvector extension enabled. One CREATE EXTENSION vector; statement on day one. Now I have a vector column type and the <-> operator for cosine distance (or <#> for inner product, or <+> for L1, your embedding model docs will tell you which to use). I create an HNSW index (a hierarchical graph structure for fast nearest-neighbor search, if you want to look it up later) on the embedding column once I have meaningful data, and I size it for the corpus I expect, not the corpus I have.

Secrets in Secrets Manager, encryption keys in KMS. Nothing (and I mean nothing) is in environment variables or in the application code. The database password is in Secrets Manager. Bedrock API patterns, if I'm using any, are in Secrets Manager. The Cognito client secret is in Secrets Manager. Every Lambda has IAM permission to read exactly the secrets it needs and nothing else. The KMS key that encrypts those secrets has a key policy that names which roles can use it.

Encryption at rest is on for the RDS instance, for the S3 buckets that hold artifacts and backups, for the EBS volumes underneath. All using KMS-managed keys, all with key rotation enabled. Default. Boring. Standard.

Auth and tenancy live next to this. I covered the row-level security shape in auth and multi-tenancy from day one. The pgvector tables get the same tenant_id column and the same RLS policy treatment, embeddings are tenant-owned data like everything else.

The schema, in concrete

Let me make it concrete. A legal pro is productizing her contract-review playbook. The data layer for that product looks something like this.

There's a tenants table, one row per consultant. There's a users table, one row per customer of a consultant. Standard. Both have tenant_id (well, tenants is the tenant) and the RLS policy described in the previous piece.

There's a documents table holding contracts the customer has uploaded. id, tenant_id, user_id, title, source_url, uploaded_at, status fields. Boring relational.

There's a corpus_chunks table holding the legal pro's own body of work, her playbook, her annotated examples, her clause library. Each row: id, tenant_id, source_id, chunk_text, chunk_metadata jsonb, embedding vector(1024). The embedding column is where pgvector earns its keep.

There's a conversations table and a messages table holding the customer-facing chat sessions. Tenant-owned, user-scoped, audit-tracked.

There's an audit_events table (same one from the previous piece) recording every state change with tenant, user, action, evidence reference.

That's the whole data model for a working MVP. Six tables and change. All in one Postgres. All under one set of credentials, one backup, one upgrade. When a contract comes in for review, the application does a vector similarity search against corpus_chunks filtered by tenant_id to find the most relevant pieces of the legal pro's playbook, joins to documents for the contract text, and feeds the whole thing to the model. One query. One database. One mental model.

Swap legal pro for a career coach reviewing resumes against her positioning approach and the schema barely changes. The documents table holds resumes instead of contracts. The corpus_chunks table holds her career-frameworks instead of contract clauses. Same shape. Same code path. The architecture spine carries any consultant vertical without rework.

Secrets, the boring way

I want to dwell on secrets management for a minute, because this is where MVP teams cut corners and pay later.

Every secret my application needs is fetched at runtime from Secrets Manager. Not at build time. Not baked into a container image. Not put in an environment variable by the CI/CD pipeline. At runtime, by code that has IAM permission to read that specific secret.

Why does this matter? Because secrets in environment variables show up in logs the moment someone calls printenv for debugging. Secrets baked into container images travel with the image, and images get pushed to registries, pulled to developer laptops, shared in screenshots. Secrets that have to be rotated manually never get rotated. Secrets that auto-rotate with a Secrets Manager rotation policy get rotated every 30 or 60 days without anyone having to remember.

The KMS piece sits underneath. Secrets Manager encrypts each secret with a KMS key. The KMS key's policy says which IAM roles can use it. Now to read a secret you need both an IAM permission on the secret AND a KMS permission on the key. Two layers. Either one slammed shut stops the read.

This is what people mean by "defense in depth" and it costs almost nothing to do on day one. If you set it up later, you have to find every place that reads a secret, change the code path, redeploy, and verify. If you set it up on day one, every new bit of code that touches a secret goes through the right path because that's the only path that exists.

Why Aurora can wait

People will tell you to use Aurora instead of RDS. Aurora is great. Aurora is also more expensive, more complicated to operate, and overkill for the MVP shape.

Aurora's killer feature is the separation of storage and compute, you can scale read replicas independently, you can fail over in seconds, you get continuous incremental backups. All of that is real and useful at scale. None of it is the bottleneck for a single-consultant MVP serving its first ten customers.

RDS Postgres with point-in-time recovery is plenty. The backup window is automatic. You can restore to any second in the last seven days (or thirty if you want to pay for it). You can promote a read replica if you eventually need one. The migration path from RDS Postgres to Aurora Postgres is well-trodden and the schema is identical, you can swap when you outgrow it. You will not have to rewrite anything.

The same logic applies to dedicated vector databases. The day you have a hundred million embeddings and your pgvector queries are getting slow, you migrate the embeddings to a dedicated store and keep the relational data in Postgres. That migration is much smaller than the "stand up the whole platform on two stores from day one" project. You earn the right to specialize by outgrowing the simpler thing.

The parts that will bite you

A few things I've stepped on.

Embedding dimension changes break everything. When you change your embedding model (say, moving from a 768-dim model to a 1024-dim model) your existing embeddings become useless. Plan for this. The corpus_chunks table should carry the model name alongside the embedding, and you should have a re-embed job ready. You will change models at least once in the first year.

pgvector indexes take memory. HNSW indexes live in RAM. When the corpus grows, your instance's memory needs grow. Watch this metric in CloudWatch. The day you start swapping is the day queries get slow. Bump the instance class one tier up before that day, not after.

Secrets Manager calls aren't free. Each Lambda invocation that fetches a fresh secret is an API call you pay for. The fix is the AWS Parameters and Secrets Lambda Extension, a sidecar that caches secrets in-process. Configure it once, save money forever.

Backups have to be testable. A backup you've never restored is not a backup. Once a month, restore the latest snapshot into a scratch RDS instance and run a smoke test against it. The day you actually need to restore is the wrong day to find out the backup is corrupt or the schema migration tool doesn't roll forward cleanly.

Don't forget RLS on the new tables. Every new table needs the tenant_id column and the RLS policy. There's no compiler that enforces this. Make it a checklist item on the schema migration template. The day someone adds a table without it is the day you get a cross-tenant leak, and you will not find it from monitoring.

What this costs

A db.t4g.medium RDS Postgres with 50 GB of gp3 storage and seven-day point-in-time recovery costs roughly the price of a dinner per month. Secrets Manager bills per secret per month and per ten thousand API calls, pennies. KMS bills per key per month and per request, also pennies. S3 for backups bills per GB per month, pennies again until you have real volume.

For a single-consultant MVP serving a few dozen customers, the entire data layer comes in under a hundred dollars a month. For the equivalent setup on a managed vector DB plus a separate Postgres, you're often looking at four to five times that. The one-store version is cheaper and operationally simpler at the same time. Those are usually opposing forces; here they line up.

If you're building this and you remember nothing else: one Postgres with pgvector, secrets in Secrets Manager with KMS, encryption on everywhere by default, tenant_id and RLS on every table. Wire that on day one and you've earned the right to ignore the data layer for a long time.