Day 30: Project: Multi-Tenant Code RAG on Bedrock Knowledge Bases

Day 30: Project: Multi-Tenant Code RAG on Bedrock Knowledge Bases

Today you build a SaaS-shaped code assistant: each tenant’s corpus is isolated, retrieval is filtered server-side, citations fail closed, and evals run per tenant. Shared prompt soup is not isolation. Day 30 is a project day — ship something you can demo with two tenants and a CI gate that proves zero cross-hits.

⚡ TL;DR: One KB per tenant or shared index with mandatory tenant_id metadata filters enforced from the JWT (never from the model). Per-tenant golden packs. Webhook ingest per connected repo. IAM that cannot read another tenant’s prefix.

Lab / project goals

  1. Ingest two fake tenants (acme, globex) into separate S3 prefixes
  2. Retrieve with hard filters derived from auth
  3. Cite or refuse using Day 26’s gate
  4. Run eval packs per tenant with zero cross-tenant retrieval hits
  5. Document offboarding: delete prefix + index + keys

Architecture sketch

Git webhook → validate → SQS(tenant_id, paths, sha)
  → embed worker → s3://corp-rag/{tenant_id}/...
  → KB sync or OpenSearch upsert (metadata tenant_id, git_sha)
API → authZ → Retrieve(filter tenant_id=jwt.tid) → Converse → citation gate → response
def retrieve(tenant_id: str, question: str, k: int = 8):
    assert tenant_id and tenant_id.replace("-", "").isalnum()
    return bedrock_retrieve(
        knowledge_base_id=KB_ID,
        query=question,
        filter={"equals": {"key": "tenant_id", "value": tenant_id}},
        k=k,
    )

Clients must not pass tenant_id as a free form field. Take it from the verified JWT only.

Isolation options

Pattern Pros Cons
KB / index per tenant hard isolation quotas, ops cost
Shared index + filter cheaper filter bugs = breach
Cell per enterprise blast-radius control more platform work

Start shared+filter for SMB; dedicated KB/cell for enterprise. Regardless, add an automated cross-tenant probe in CI.

def test_no_cross_tenant():
    for q in load("eval/acme.jsonl"):
        paths = retrieve("acme", q["question"])
        assert all(meta_tenant(p) == "acme" for p in paths)

Project idea: dual-tenant bakeoff

Seed each tenant with deliberately similar file names (auth.ts) but different implementations. Ask “how do we verify JWTs?” for each tenant and assert citations never cross. Add a freshness footer (git_sha) per Day 27.

Deliverables checklist

  • [ ] Tenant id from JWT only
  • [ ] Metadata tenant_id on every chunk
  • [ ] Citation gate + freshness sha footer
  • [ ] Per-tenant eval in CI with cross-hit assertion
  • [ ] Offboarding runbook tested once
  • [ ] IAM policies reviewed for prefix isolation

Series navigation

← Day 29 · Day 31 →

Last updated September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply