Hardening a Developer Portfolio

📚 Series Background: This is a follow-up to where I built the foundation for what would become a comprehensive developer platform. This series documents the security hardening, performance optimization, and production features that transformed a simple portfolio into an enterprise-ready system

After building the initial portfolio, I systematically addressed security vulnerabilities, performance bottlenecks, and scalability concerns to create a production-ready platform. What started as a simple site now includes comprehensive monitoring, automated security headers, background job processing, and enterprise-grade features.

This series documents the patterns and decisions that took it from prototype to production.

Hardening a developer portfolio isn't just about adding features—it's about systematically addressing security, performance, and scalability. This post covers CSP implementation, rate limiting, INP optimization (664ms → <200ms), and GitHub integration. All with zero external dependencies where possible.

Production Features Overview

Current Architecture (December 2025):

Security: CSP headers, rate limiting, input validation, automated vulnerability scanning

Analytics: Redis-powered view tracking, trending posts, milestone detection

Performance: Server components, edge caching, image optimization, Core Web Vitals monitoring

Infrastructure: Background jobs with Inngest, GitHub integration, automated deployments

Monitoring: Sentry error tracking, Vercel Analytics, comprehensive logging

The Journey

Part 1: Security Hardening

Content Security Policy (CSP) Implementation

The site had zero CSP headers, making it vulnerable to XSS attacks and clickjacking. I implemented a defense-in-depth approach using Next.js middleware and Vercel configuration.

What I Built:

Dynamic CSP with unique nonce generation per request

Next.js middleware (src/middleware.ts) for HTML routes

Two-layer architecture: dynamic middleware + static vercel.json fallback

Support for third-party scripts (Vercel Analytics, Speed Insights)

Key Learnings:

Middleware runs on every request, so keep it lightweight (34.2 kB bundle)

Nonces are critical for inline scripts in modern frameworks

Testing CSP requires browser DevTools and careful header inspection

Vercel's CDN can strip headers; middleware provides reliable enforcement

Technical Highlights:

// Generate unique nonce per request
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

// Build strict CSP
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' https://va.vercel-scripts.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`
.replace(/\s{2,}/g, ' ')
.trim();

response.headers.set('Content-Security-Policy', csp);

Results:

XSS attack mitigation

Clickjacking protection via frame-ancestors 'none'

Strict resource loading policies

All builds passing with middleware enabled

Content Security Policy is your first line of defense against XSS attacks. Use a two-layer architecture: dynamic middleware for HTML routes + static vercel.json fallback. Generate unique nonces per request and support third-party scripts with explicit allowlisting.

Rate Limiting Implementation

The contact form API endpoint had no rate limiting, making it vulnerable to spam and abuse. I implemented a zero-dependency, in-memory rate limiter.

What I Built:

Custom rate limiter utility in src/lib/rate-limit.ts (146 lines)

IP-based tracking with Vercel header support (X-Forwarded-For)

Standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)

Graceful 429 responses with retry timing

Automated test suite (scripts/test-rate-limit.mjs)

Key Learnings:

In-memory storage works for single-instance deployments (Vercel serverless)

For multi-region or high-scale, upgrade to Vercel KV or Upstash Redis

Rate limit headers improve client-side UX (show retry time)

IP-based tracking requires careful proxy header handling

Technical Highlights:

// Rate limiter with automatic cleanup
export class RateLimiter {
private requests = new Map<string, RequestRecord>();

async check(identifier: string): Promise {
const now = Date.now();
const record = this.requests.get(identifier);

// Check if rate limited
if (record && record.requests.length >= this.limit) {
const oldestRequest = record.requests[0];
if (now - oldestRequest < this.windowMs) {
return {
success: false,
limit: this.limit,
remaining: 0,
reset: new Date(oldestRequest + this.windowMs),
};
}
}

// Allow request and update tracking
return this.recordRequest(identifier, now);
}
}

Results:

Contact form protected from spam (5 requests per 15 minutes)

Zero external dependencies

Automated testing with npm run test:rate-limit

Graceful user feedback on rate limit

Custom rate limiter = 146 lines, zero external dependencies. In-memory storage works perfectly for single-instance serverless deployments. Add standard headers (X-RateLimit-*) for better client UX. Upgrade to Redis only when you need multi-region or high-scale.

Part 2: Performance Optimization

INP (Interaction to Next Paint) Improvements

Google's Core Web Vitals showed poor INP scores (664ms+) on navigation links. The culprit: Next.js hover prefetching blocking the main thread.

What I Fixed:

Disabled hover prefetching on `` components (prefetch={false})

Added CSS performance hints (will-change-auto, contain: layout style)

Hardware-accelerated transitions (transform: translateZ(0))

Non-blocking theme toggle using React's useTransition

Non-blocking form submissions

Key Learnings:

Hover prefetching is great for UX but terrible for INP

Links still prefetch on viewport intersection (better trade-off)

CSS containment prevents expensive layout recalculations

React 18+ useTransition keeps UI responsive during state updates

Technical Highlights:

// Non-blocking theme toggle
export function ThemeToggle() {
const [isPending, startTransition] = useTransition();
const { setTheme, theme } = useTheme();

const toggleTheme = () => {
startTransition(() => {
setTheme(theme === 'light' ? 'dark' : 'light');
});
};

return (

{/* ... */}

);
}

Results:

INP score improved from 664ms+ to under 200ms (Good)

Smoother navigation and interactions

Theme toggle feels instant

No layout shifts or visual jank

Next.js hover prefetching can block the main thread and destroy INP scores (664ms+). Disable with prefetch={false} on `` components. Links still prefetch on viewport intersection—a better trade-off that improved INP to <200ms (Good).

GitHub Contributions Heatmap

I added a live GitHub contributions heatmap to the homepage to showcase activity.

What I Built:

GitHub GraphQL API integration (/api/github-contributions)

Client-side caching (24-hour duration) via localStorage

Fallback to sample data if API unavailable

Visual heatmap using react-calendar-heatmap

Optional GITHUB_TOKEN env var for higher rate limits

Key Learnings:

GitHub's public API has low rate limits (60 req/hour)

Personal access tokens (no scopes needed) bump it to 5,000 req/hour

Client-side caching is essential for API rate limit management

Always have fallback data for better UX

Technical Highlights:

// Fetch with caching
const fetchContributions = async (): Promise => {
// Check cache first
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const data = JSON.parse(cached);
if (Date.now() - data.timestamp < CACHE_DURATION) {
return data;
}
}

// Fetch from API
const response = await fetch('/api/github-contributions');
const data = await response.json();

// Cache result
localStorage.setItem(
CACHE_KEY,
JSON.stringify({
...data,
timestamp: Date.now(),
})
);

return data;
};

Results:

Live contribution data on homepage

24-hour client-side cache reduces API calls

Graceful fallback to sample data

Under 5KB bundle size increase

Part 3: Developer Experience

AI Contributor Guide

I created comprehensive instructions for AI coding assistants (GitHub Copilot, Cursor, etc.) to maintain code quality and architectural consistency.

What I Built:

Detailed guide in .github/copilot-instructions.md

MCP (Model Context Protocol) server guidelines

Stack decisions, architecture patterns, and conventions

Key Learnings:

AI assistants benefit from explicit architectural constraints

Document "what not to change" as much as "how to build"

Include import alias patterns and file organization rules

MCP servers enable secure, local-first AI integrations

Results:

Consistent code quality across AI-assisted changes

Clear boundaries and conventions

Faster onboarding for contributors

Reduced architectural drift

Key Takeaways

Security is not optional: CSP and rate limiting should be in every production app

Performance is perception: 664ms feels slow, under 200ms feels instant

Zero dependencies are underrated: Custom rate limiter = 146 lines, no external deps

Caching strategy matters: Client-side cache + fallback data = resilient UX

Document everything: Future you will thank present you

AI assistants need guardrails: Explicit conventions prevent drift

What's Next?

Taking a project from "works on my machine" to "production-ready" requires intentional hardening. Security, performance, and developer experience all need attention.

The good news? Modern frameworks like Next.js make it easier than ever. Middleware for CSP, server actions for rate limiting, and React's concurrent features for performance, the tools are there.

The platform is now deployed in production with comprehensive monitoring, automated security scanning, and enterprise-grade features. The architecture patterns documented here have proven themselves in real-world usage with excellent Core Web Vitals scores and zero security incidents.

Now it's your turn. What production features does your developer platform need?