AWS Blocks and the hard 20%
I built a document-grounded RAG chat app on AWS Blocks, the new open-source TypeScript framework for backend-on-AWS without the infrastructure work. The pitch is appealing: local-first development, no AWS account to start, and the same code deploys to managed services unchanged. It even ships AI primitives, an Agent block and a KnowledgeBase block, so a RAG app is a few imports rather than a week of plumbing.
For most of the build, the pitch held. Then I hit the part frameworks always struggle with, and it’s worth being precise about where the line is.
Where it held
The easy 80% really is easy. Auth, file storage, a knowledge base, an agent with streaming and tool calls, all of it ran locally with no AWS account and no Docker. The KnowledgeBase block indexed a folder of documents and gave me semantic retrieval behind a single retrieve() call. The type safety runs from the data layer to the frontend with no code generation step. For a working RAG demo, this is genuinely fast.
I wired per-user document libraries on top, with retrieval isolation by metadata filter, and a tool-calling agent that searched before answering and cited its sources. Locally, you upload a document and it’s searchable a second later. It feels like magic.
Where it leaked
The feature I most wanted was “upload a document, then chat with it.” Locally, that worked. In production, it didn’t, and the reason is structural, not a bug.
The KnowledgeBase block has exactly one runtime method: retrieve(). There’s no add() or ingest(). Ingestion happens at deploy time:
cdk deploy → syncs ./knowledge to S3 → Bedrock ingests
On AWS, the Lambda filesystem is read-only, and the managed Bedrock knowledge base only ingests the corpus when you deploy. So a document a user uploads at runtime gets stored, but never indexed. The local dev server papers over this by re-reading the folder live, which is exactly what makes the gap invisible until you ship.
This is the leaky abstraction in its natural habitat. The block models a deploy-time corpus beautifully. It does not model runtime ingestion, because runtime ingestion is the genuinely hard part, and no abstraction makes a hard problem disappear. It just decides whether to hide it.
What I’d actually do
There are two honest paths, and the framework’s escape hatch matters here.
The first is to make ingestion dynamic: write embeddings to a runtime-writable vector store on upload instead of relying on deploy-time ingestion. AWS Blocks doesn’t ship a dedicated vector block, but its Postgres block exposes raw SQL, so pgvector is available. That gives you a real, runtime-writable vector index with metadata filtering, at the cost of the framework’s ~$0 idle story, since Postgres carries an idle floor.
The second is to not pretend. Ship a curated corpus that’s ingested at deploy time, which is exactly what the block is good at, and drop the upload feature rather than deploy it broken. For a portfolio piece whose whole point is reliability, a feature that silently fails in production is worse than a smaller feature that works. I went this way.
The takeaway
AWS Blocks is a good answer to a real question, and I’d reach for it again for greenfield apps, prototypes, and AI side projects where local-first speed matters. The lesson isn’t “abstractions are bad.” It’s the usual one, stated plainly: the framework makes the common 80% trivial, and the remaining 20% is still yours. The skill is knowing, before you commit, which 20% you’re signing up to own.