init: samartha-sr.com
I've always wanted my own website and there are a couple options when you want to build it: Github has Github Pages, Notion has Notion Sites, your nan would probably know about WordPress but there's something innately satisfying about creating, nurturing, and maintaining your own (website).
The Tech Stack
No NextJS. Thank you.
Yup, not jumping on that bandwagon after working with it for the past few years. Why?
I really want to believe that there's something out there which doesn't try to abstract everything, bloating itself up and leaves out some of the complexity for the developers. I don't want to think about SSR, CSR, ISR, SSG, and whatever the next latest three letter acronym is.
The next motivation was to just learn something other than a React framework. So here's what I landed on:
- SvelteKit with adapter-node for SSR (I know I'm directly contradicting myself from a few sentences ago but hey, at least I know I'm explicitly choosing SSR)
- Prisma / PostgreSQL on AWS RDS
- AWS App Runner to host the app
- CloudFront as the CDN in front of everything
- S3
- Route53 for DNS, ACM for SSL certificates
- GitHub Actions for CI/CD
- Docker for containerizing the app
- Terraform as IaC
Challenges faced
Prisma v7 and PostgreSQL Compatibility
This one caught me completely off guard. Prisma v7 introduced a breaking change: you now need to explicitly provide a driver adapter to connect to PostgreSQL. The old "just give me a connection string and I'll figure it out" approach no longer works.
The fix was installing @prisma/adapter-pg and wiring it up:
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
But it didn't stop there. I was using the prisma-client generator name (the new default in v7), which caused issues with custom output paths during the build. Switching back to prisma-client-js fixed it.
On top of that, PostgreSQL 18 changed its default data directory to /data, which conflicted with some Docker base image assumptions. Small thing, but it added another half-hour of head-scratching to the debugging session.
The Database That Refused to Connect
After deploying to App Runner, the app would start but immediately crash with a database connection error.
Turns out there were two paths to fixing this:
- Disable SSL verification — quick and dirty, works for development
- Use the RDS certificate bundle — the proper solution
I went with option 2. AWS RDS requires SSL by default, and the Node.js PostgreSQL driver needs to know about the RDS root certificate. I added a step in the Dockerfile to download the RDS global certificate bundle:
RUN mkdir -p /certs && wget -O /certs/global-bundle.pem \
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
Then configured the database connection to use it. Once SSL was properly configured, connections worked perfectly.
DNS That Refused to Propagate
This was the most infuriating one. Everything was deployed. App Runner was running. CloudFront was configured. ACM certificate was validated. Route53 had the right records. I updated the nameservers at my registrar. And then... nothing. The domain just wouldn't resolve.
I waited. I ran dig commands. I ran nslookup commands. I checked and rechecked every record. I even recreated the entire hosted zone thinking something was corrupted.
After ridiculous amounts of debugging, I discovered the culprit: Pi-hole 🙂. My DNS ad blocker was intercepting and blocking cached DNS queries which were resolving to a common namecheap lander IP.
The fix was embarrassingly simple:
- Disable Pi-hole temporarily
- Flush the DNS cache
- Re-enable Pi-hole
Instantly everything resolved. Hours of debugging, defeated by my own ad blocker. Sadge.
The Latency Issue
With everything finally working, I loaded the site and... it was noticeably slow. Pages were taking 500-800ms to load, which is unacceptable for what is essentially a static-ish blog.
The problem was architectural. I initially deployed everything in ap-south-1 (Mumbai), and CloudFront's PriceClass_100 was routing through distant edge locations. Every single request was going all the way to the App Runner origin because CloudFront's default cache behavior was set to CachingDisabled. CloudFront was acting as a pure pass-through proxy — all the CDN infrastructure, none of the CDN benefits.
The fix had two parts:
First, counterintuitively even, I moved all resources from ap-south-1 to us-east-1. This is where CloudFront has the most edge locations, and the ACM certificate for CloudFront was already required to be in us-east-1 anyway — which I believe why it was slow in the first place as I feel that there were intercontinental hops happening.
Step 1: terraform destroy
Step 2: Update the region variable
Step 3: terraform apply
Step 4: profit
Second, I enabled caching. I switched CloudFront's default cache policy from CachingDisabled to CachingOptimized, and added Cache-Control headers to all SSR page responses:
setHeaders({ 'cache-control': 'public, max-age=60, s-maxage=300' });
This tells CloudFront to cache pages for 5 minutes at the edge (s-maxage=300) while browsers cache for 1 minute (max-age=60). The first visitor to each edge location gets the slow origin fetch, but everyone after that gets a near-instant response from cache.
The result: pages that were taking 800ms now load in under 50ms from CloudFront cache. It's still a bit flakey on cache misses (cold edges), but the overall experience is dramatically better.
The Architecture
Here's how everything fits together:
User → CloudFront (CDN + SSL) → App Runner (SvelteKit SSR) → RDS (PostgreSQL)
→ S3 (images, served via /images/*)
Terraform manages the entire stack: VPC with private subnets, security groups, RDS instance, ECR repository, App Runner service with VPC connector, S3 bucket with OAC, CloudFront distribution, Route53 hosted zone, and ACM certificate.
GitHub Actions handles CI/CD. Push to main, and it builds a Docker image, pushes to ECR, and triggers an App Runner deployment. The whole pipeline takes about 3-4 minutes.
Learnings
- RTFM. Prisma v7's breaking changes around driver adapters would have been obvious if I'd read the docs before upgrading.
- Check your local environment. When debugging network issues, always consider what's between you and the internet. DNS-level ad blockers, VPNs, proxies — they all interfere.
- CDNs don't help if you don't cache. Having CloudFront in front of your app is pointless with
CachingDisabled. You're just adding a hop. - Terraform makes region migrations trivial. Destroying and recreating 29 resources in a new region took minutes, not hours. Infrastructure as code pays for itself the first time you need to make a big change.
- Start simple, optimize later. I could have spent weeks designing the perfect caching strategy upfront. Instead, I shipped it broken, measured the problem, and fixed it with a few lines of config.
This site which you are browsing right now is the result of all the work and the entire infrastructure can be torn down and recreated with just two commands, terraform destroy, terraform apply.
Disclaimer
Parts of this were written by a 🤖, but all of it has been read, edited, and validated by a 🧬.