Architecture
This portfolio was designed as more than a static website. It is a multi-service backend system built to showcase real engineering decisions, tradeoffs, and infrastructure choices through working features rather than isolated demos.
System diagram
The platform runs as three independent services behind a single Nginx reverse proxy on an AWS EC2 t3.small instance. Each service owns a specific responsibility and communicates through defined interfaces rather than shared application state. This keeps the system modular and makes it easier to scale or replace individual components over time.
The three services
The frontend and primary API layer. This service handles server-side rendering for all public pages, the hidden CLI experience, CTF flag submission endpoints, and Stripe payment routes. Next.js was chosen over a client-side SPA primarily for SEO and performance because search engines and visitors receive fully rendered HTML immediately instead of waiting for client-side hydration.
The frontend also acts as the orchestration layer between the browser and the backend services. It routes requests to the AI and real-time systems while keeping the public-facing interface unified behind a single domain.
The AI and game service. It powers the RAG assistant on the /assistant page as well as the Lost Explorer text adventure hidden inside the terminal experience. The RAG pipeline uses FAISS for vector similarity search and HuggingFace sentence-transformers for embeddings, while Groq handles LLM inference.
Python was the natural choice for this service because the modern AI ecosystem is heavily Python-centric, with significantly stronger tooling and library support than other ecosystems. The assistant itself is built around a curated corpus containing project information, technical background, and site structure data. Instead of relying only on prompting, the system retrieves relevant context before generating responses so answers remain grounded in actual portfolio content.
The real-time service powering the community canvas at /draw. Visitors verify with an email magic link and draw together on a shared pixel canvas in real time through persistent WebSocket connections. The service also handles canvas persistence in Redis, email verification sessions, canvas snapshots, and a growth mechanic that expands the canvas as it fills up. Go was selected for this service because its concurrency model maps naturally to real-time workloads.
Goroutines are lightweight enough to dedicate per connection, and channels provide a clean model for broadcasting events across rooms and players. Separating the real-time system into its own service also isolates latency-sensitive WebSocket traffic from the rest of the application stack.
Data layer
Redis is used for short-lived and high-speed application state, including per-IP rate limiting, daily API budget tracking for the AI service, Lost Explorer session state, canvas pixel data, email verification tokens, canvas sessions, canvas snapshots, and the email ban list for community canvas.
Redis was chosen over in-memory state because the data survives service restarts and remains accessible across all services without tightly coupling them together.
PostgreSQL stores the platform’s persistent relational data, including CTF submissions, the Hall of Curiosity leaderboard, canvas stroke audit logs tied to verified emails, and Stripe payment records. It provides a stable and well-understood foundation for structured application data with clear schemas and transactional guarantees.
FAISS runs directly inside the FastAPI process as an in-memory vector index for the RAG assistant. At the scale of a personal portfolio, introducing a dedicated managed vector database would add unnecessary operational complexity.
FAISS provides fast retrieval with minimal infrastructure overhead and rebuilds automatically from source files whenever the service restarts.
Infrastructure and deployment
All services run on a single AWS EC2 t3.small instance behind Nginx. Nginx is responsible for SSL termination through Let’s Encrypt, reverse proxying, and request-level protections such as route-specific rate limiting.
The domain is registered through Namecheap and points directly to the EC2 instance through an A record. PM2 manages all long-running processes and automatically restores them after server reboots.
Deployments are automated with GitHub Actions. Pushing to the main branch triggers a CI/CD workflow that connects to the server over SSH, pulls the latest changes, rebuilds the Next.js application, and restarts the relevant PM2 processes.
The infrastructure intentionally avoids Docker and Kubernetes at this stage because, for a single-instance deployment, the added operational overhead would provide limited practical benefit.
Security considerations
None of the internal services are directly exposed to the public internet. Each service runs on a private internal port and is only reachable through Nginx. Secrets and API keys are stored exclusively in environment variables and never committed to the repository.
The AI assistant enforces layered rate limiting. Nginx filters excessive traffic before it reaches the application layer, while Redis tracks request activity per IP inside the FastAPI service itself. A daily budget cap also prevents abuse from exhausting external AI API quotas.
CTF flags are distributed across different layers of the system. Some are discoverable through normal exploration of the site. Others require interacting with specific services or completing challenges. All flag validation is performed server-side.
What I would change at scale
The current architecture is intentionally pragmatic and optimized for a personal project rather than high-scale production traffic. Running all services on a single EC2 instance keeps infrastructure simple while still preserving clear service boundaries.
Under heavier production workloads, the next step would be separating services onto independent instances, introducing a dedicated load balancer such as AWS ALB, and migrating PostgreSQL and Redis to managed infrastructure like RDS and ElastiCache.
The services were designed to remain largely stateless, making horizontal scaling possible without major architectural rewrites.
I would also introduce Docker earlier in the development lifecycle. PM2 works well for a small deployment, but containerization would simplify local development, dependency management, and deployment consistency while providing a cleaner path toward orchestration if the system continued to grow.