Module 13 · Cloud & System Design · Deep Dive
You already shipped DocChat to Vercel and Neon with a few clicks. Now learn the path most UAE enterprises actually run on — AWS — and why the extra control is worth it when data residency, compliance, and scale are on the line.
BasicIntermediateBuild
On Vercel you pushed to git and a URL appeared. The platform decided where your code ran, how it scaled, and how it talked to Neon. That's a Platform-as-a-Service (PaaS) — opinionated, fast, and it hides the machinery.
AWS is the machinery. It's a menu of ~200 building blocks — compute, storage, networking, databases — that you assemble. More work, but total control: you pick the region, the network boundaries, who can touch what, and exactly how much you pay.
| Concern | Vercel/Neon (you did this) | AWS (this module) |
|---|---|---|
| Run the API | Auto from git push | App Runner / ECS Fargate / EC2 |
| Database | Neon (managed Postgres) | RDS / Aurora (managed Postgres) |
| File storage | Blob / external | S3 |
| Secrets | Project env vars | Secrets Manager / Parameter Store |
| Who decides scale & region | The platform | You |
Identity and Access Management is the front door to your entire account, and it's the single most important thing to understand. Get it wrong and you either lock yourself out or — far worse — leave the door wide open. Three concepts:
A policy is allow/deny rules. Here's one that lets DocChat's container read and write objects in only its own bucket — nothing else:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::docchat-uploads/*"
}
]
}
AdministratorAccess to an app role, and never use the root account for daily work. Notice the policy above does not say s3:* and does not say Resource: "*" — it names the exact actions and the exact bucket. That specificity is the difference between a controlled blast radius and a breach.
AWS is physically organised into Regions — geographic areas like us-east-1 (Virginia) or eu-west-1 (Ireland). Each Region contains multiple Availability Zones (AZs): separate data centres with independent power and networking, a few kilometres apart. Spreading across AZs is how you survive one data centre failing.
me-central-1 (UAE, based in the Emirates). There's also me-south-1 (Bahrain) nearby in the Gulf. For UAE banks, government, and healthcare, regulations often require customer data to physically stay inside the country. Deploying DocChat to me-central-1 means your uploaded PDFs and database never leave the UAE — exactly what a compliance officer needs to hear. This is a genuine differentiator: "I'd pick me-central-1 for data residency" shows you understand the local market.
Pick your Region before you build — every resource lives in one. The tradeoffs: latency to your users, data-residency law, and which services are available (newer Regions sometimes lag on niche services). For a UAE product serving UAE users, me-central-1 wins on both latency and law.
On Vercel you never chose this — the platform did. On AWS you pick from a spectrum, trading control for convenience:
# A raw Linux server in the cloud. You manage the OS, patches, scaling.
aws ec2 run-instances --image-id ami-0abc --instance-type t3.micro
Maximum control, maximum responsibility. Choose EC2 when you need a specific OS, GPUs, or long-running stateful processes. For a stateless web API it's usually too much work.
You already containerised DocChat with Docker in Module 7. Hand AWS that same image and it runs it — no servers to patch. Fargate runs containers with no VMs to manage; App Runner is even simpler: point it at your image, it builds, deploys, scales, and gives you HTTPS. App Runner is the closest thing to the Vercel feeling, and it's the right default for DocChat's FastAPI container.
def handler(event, context): return {"statusCode": 200, "body": "hi"}
Lambda runs a single function on demand — you pay per invocation, scale to zero when idle. Put API Gateway in front to turn HTTP requests into Lambda calls. Brilliant for spiky, event-driven work. The catch for DocChat: a 15-minute max runtime and "cold starts" make it awkward for a heavy RAG request that loads a model. Use Lambda for the thumbnail-generation or webhook around DocChat, not the core API.
Rule of thumb: spiky/event-driven → Lambda · a normal web service → App Runner/Fargate · special OS or hardware → EC2. Knowing when to use each is the interview skill, not memorising flags.S3 (Simple Storage Service) stores objects (files) in buckets. It's effectively infinite, cheap, and durable — the natural home for DocChat's uploaded PDFs. Buckets are private by default, and you should keep them that way; access is granted through IAM policies and signed URLs, never by making the bucket public.
The slick pattern is the presigned URL. Instead of routing a 40 MB PDF through your FastAPI server (slow, doubles your bandwidth), your backend hands the browser a temporary, signed URL and the browser uploads directly to S3:
# Backend asks S3 for a one-time upload URL import boto3 s3 = boto3.client("s3", region_name="me-central-1") url = s3.generate_presigned_url( "put_object", Params={"Bucket": "docchat-uploads", "Key": "u123/report.pdf"}, ExpiresIn=300, # URL valid for 5 minutes ) # Send `url` to the browser; it does a PUT straight to S3.
Neon gave you managed Postgres on Vercel's side. On AWS the equivalent is RDS (Relational Database Service) — managed Postgres with automated backups, patching, and failover. Aurora is AWS's higher-performance Postgres-compatible engine in the same family.
Crucially for DocChat: pgvector is available on RDS and Aurora PostgreSQL. You enable it just like anywhere else, so your entire RAG schema from Module 8 ports over unchanged:
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE chunks ( id bigserial PRIMARY KEY, doc_id text, content text, embedding vector(1536) );
Run RDS in private subnets so it's never reachable from the public internet — only your App Runner / Fargate service, inside the same VPC, can connect. Then deploy DocChat's FastAPI container to App Runner:
# Push your existing Docker image to ECR (AWS's registry)... aws ecr create-repository --repository-name docchat-api # ...then point App Runner at it. It builds, deploys, and gives you HTTPS.Same code, new home: the FastAPI app, the pgvector queries, the embeddings — all identical to your Vercel/Neon build. Only the surrounding plumbing changed.
Secrets Manager & Parameter Store. Never hardcode your database password or API keys. Store them in Secrets Manager (encrypted, rotatable) or Parameter Store (free for plain strings), and let your container's IAM role fetch them at runtime. No secrets in the image, no secrets in git.
CloudWatch. Every print/log from your container flows to CloudWatch Logs, and metrics (CPU, request count, errors) land in CloudWatch Metrics where you set alarms. This is your console.log and your dashboard. When DocChat 500s in production, CloudWatch is where you look.
VPC — just enough. A Virtual Private Cloud is your private network. The key idea: public subnets (reachable from the internet — your App Runner endpoint) and private subnets (database, internal services). Security groups are per-resource firewalls — e.g. "RDS only accepts port 5432 from the app's security group." That one rule is what keeps your database off the open internet.
Clicking around the AWS console to create resources is fine for learning and fatal for production — nobody can reproduce it, and nobody remembers what you did. Infrastructure as Code (IaC) describes your whole stack in version-controlled files you can apply, review in a PR, and tear down cleanly.
Two leading choices in June 2026: Terraform (HashiCorp's declarative HCL, cloud-agnostic) and AWS CDK (define infra in real Python/TypeScript). A taste of Terraform for DocChat's bucket:
resource "aws_s3_bucket" "uploads" { bucket = "docchat-uploads" } resource "aws_s3_bucket_public_access_block" "uploads" { bucket = aws_s3_bucket.uploads.id block_public_acls = true block_public_policy = true }
Now the bucket — private by default, enforced — lives in git. A teammate runs terraform apply and gets an identical environment in me-central-1. That reproducibility is what "production-ready" actually means.
me-central-1 for data residency." That single sentence signals seniority.
The request flow, end to end:
DocChat on AWS — the shape
# 1. Browser wants to upload a PDF Browser ─▶ App Runner (FastAPI) ─▶ generate_presigned_url() # 2. Browser uploads the PDF DIRECTLY to S3 with that URL Browser ─────────────────────────▶ S3 (docchat-uploads, private) # 3. App Runner reads the PDF, chunks + embeds it, writes vectors App Runner ─▶ RDS Postgres + pgvector (private subnet) # 4. Secrets & observability throughout App Runner ─▶ Secrets Manager (DB password, API keys) App Runner ─▶ CloudWatch (logs + metrics + alarms)
And the boto3 snippet that powers step 1 — the heart of the whole flow, with the IAM role (not access keys) supplying credentials automatically:
app/storage.py
import boto3 s3 = boto3.client("s3", region_name="me-central-1") def presigned_upload(user_id: str, filename: str) -> str: """Return a short-lived URL the browser can PUT to.""" key = f"{user_id}/{filename}" return s3.generate_presigned_url( "put_object", Params={ "Bucket": "docchat-uploads", "Key": key, "ContentType": "application/pdf", }, ExpiresIn=300, )
No credentials in that code — App Runner runs under an IAM role scoped to exactly s3:PutObject on docchat-uploads/*. Least privilege, presigned, private, and resident in the UAE. That's the production shape.
Answer from memory — retrieval is what moves this from "I read it" to "I know it".
Which AWS Region keeps DocChat's data inside the UAE?
How should a container get access to one S3 bucket?
What lets the browser upload a PDF straight to S3?
Best compute for DocChat's normal FastAPI web service?
Which is a classic AWS bill trap to watch?