Edge-first architecture on a solo portfolio site does not mean running every function on Cloudflare Workers worldwide. When I deployed elangodev.com to Vercel, I mapped each workload to the runtime that matched its latency and capability needs. Static marketing pages and ISR blog posts cache at the edge. The AI assistant streams from an Edge function. Database-heavy blog generation and chat mutations stay on Node.js where they can run up to 60 seconds and talk to AWS S3.
Vercel as My Global Entry Point
Vercel sits in front of my Next.js 16 App Router build as a distributed reverse proxy and build pipeline. Every push to main triggers a production deployment with compressed assets, automatic HTTPS, and regional edge caching for static files. I disabled the X-Powered-By header in next.config.ts and added security headers globally — small hardening steps that apply regardless of runtime.
Blog posts use incremental static regeneration. In app/blog/[slug]/page.jsx I export revalidate = 3600, so HTML is generated at build or first request then refreshed hourly. That gives me CDN cache hits for readers and crawlers — including Gemini and other bots I explicitly allow via htmlLimitedBots — without stale content living forever.
Edge Runtime for Low-Latency Streaming
The portfolio AI chat route is the clearest Edge win. Streaming Gemini responses must begin quickly; Edge functions start closer to users and avoid the 10-second Hobby-tier ceiling that blocked my early serverless experiments. I documented the choice directly in code:
// app/api/chat/route.js
// Optimize for Hobby Plan: Use Edge runtime to bypass 10s serverless limit
export const runtime = "edge";
const result = await streamText({
model: googleProvider.languageModel("gemini-flash-latest"),
messages: normalizedMessages,
});
return result.toDataStreamResponse();
Edge is wrong for every route though. My blog AI generator sets runtime = "nodejs" and maxDuration = 60 because it chains Gemini calls, JSON validation, image generation, and S3 uploads. Trying that on Edge would hit memory and duration walls immediately.
Retry-Hardened Server Fetches to Supabase
Even Node routes execute in ephemeral regions far from my Supabase project in ap-southeast-2. Transient ECONNRESET and abort errors showed up in Vercel logs during cold starts. I wrapped the Supabase server client with fetchWithServerRetry: three attempts, exponential backoff, 20-second timeout per try.
// app/utils/supabase/server.js
const fetchWithServerRetry = async (url, options) => {
const fetchWithRetry = async (attempt = 0) => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20000);
try {
return await fetch(url, { ...options, signal: controller.signal });
} catch (err) {
if (attempt < 3 && isRetryable(err)) {
await new Promise((r) => setTimeout(r, 500 * Math.pow(2, attempt)));
return fetchWithRetry(attempt + 1);
}
throw err;
}
};
return fetchWithRetry();
};
I attach this fetch wrapper to cookie-based clients, simple clients for static generation, and the admin client. That single pipeline prevents random 500s from becoming user-visible failures across contact forms, chat sends, and cron blog jobs.
Distributed Caching Layers I Actually Use
I do not run Redis on elangodev.com yet. My caching stack is Vercel CDN for static assets, ISR for blog HTML, and browser caching for service worker assets on the PWA-enabled chat shell. Supabase connection pooling handles database concurrency. For media I serve optimized AVIF/WebP through Next Image with remote patterns whitelisted for my S3 bucket and Supabase storage.
When cache invalidation matters — after publishing a post — revalidate windows refresh content without manual purges. Event-driven purges would be next if I added on-demand revalidation webhooks from Supabase triggers.
Pipeline Thinking Without Over-Engineering
Event-driven pipelines appear in my push notification flow: insert message, enqueue web-push sends, delete expired subscriptions on 410 responses. Failures in notification delivery never roll back the message row. That is a lightweight asynchronous pipeline implemented with Promise.allSettled rather than a managed queue service — appropriate for current traffic, easy to migrate later.
I log retry attempts from fetchWithServerRetry with attempt number and delay so Vercel log drains show whether Supabase blips correlate with region outages or cold starts. That observability is my substitute for a full distributed tracing stack on a personal site — good enough to decide when to upgrade plans or move cron jobs.
Architecting edge-first on a real site means choosing runtimes per route, caching at the layer that matches data freshness, and hardening server-to-database hops against regional latency. My Vercel deployment is not theoretically perfect; it is tuned to how elangodev.com actually fails and recovers in production logs.
I also watch Supabase dashboard metrics after major traffic spikes — connection count, slow queries, Realtime fan-out — the same way I watch Vercel function duration percentiles. Edge-first does not eliminate the database as a bottleneck; it moves latency savings to the layers users feel first. That mindset keeps portfolio demos and portfolio pages fast globally even though my Postgres primary lives in Sydney. When latency spikes, I adjust revalidate windows before buying more infrastructure — often the cache was stale, not the region. Edge-first is a scheduling discipline, not a marketing badge. I document runtime choices beside each route so the next refactor does not move a sixty-second S3 job to Edge chasing hype. That inventory lives in the same README I use when onboarding contractors to elangodev.com infrastructure.
Caching on Vercel for this publication
Homepage and blog listing use revalidate windows so editorial updates appear within an hour without SSR on every hit. Post detail pages cache per slug. After publish, rewrite scripts should pair with revalidatePath calls in admin flows so featured cards refresh.
Edge caching does not fix thin content — it only serves whatever you wrote faster. I mention that explicitly because performance posts often pretend CDN config substitutes for writing. On elangodev.com, caching supports a content-first homepage; it does not replace depth.
What I cache vs what I do not
Cache: published HTML for posts, handbook shells, static legal pages. Do not cache: authenticated dashboard, chat APIs, presigned drop URLs. Mixing those concerns is how private data leaks into shared caches.
Field notes unique to architecting edge first applications distributed caching pipelines
I keep this article in the elangodev.com corpus because it captures decisions I made while shipping features on this domain or in client systems I can discuss publicly. If a section ever drifts into generic textbook language, I rewrite it with a concrete file path, metric, or failure story. That editorial rule is how we avoid another round of low-value AdSense feedback.
Readers should leave with a default recommendation, a “do not do this” warning, and a verification step. For example: change one config, deploy to preview, measure one metric, then promote. Tutorials that never tell you how to know it worked are incomplete — I try to close every guide with a verification habit I actually use.
Finally, I cross-link related handbook chapters and sibling posts sparingly. Internal links should help humans navigate a topic cluster, not manufacture a crawl graph of thin pages. After the unpublish pass, only keepers that clear our depth bar remain published and sitemap-listed.
I will continue expanding this guide as production behavior on elangodev.com changes — new failure modes, new metrics, new code paths. Treat the article as a living engineering note, not a static SEO page. That maintenance posture is part of how a real publication compounds value for readers and for crawlers evaluating originality over time.





