
A visual overview of this website’s architecture: Next.js, Payload CMS, PostgreSQL, Cloudflare R2, Docker, Dokploy, and GitHub, and how they work together to serve pages and publish content.
First of all, I’d like to explain why this website exists, especially since this will be the first of many articles about software development.
About five years ago, during the final stage of my Computer Engineering degree, I remember developing complex distributed systems, designing massive databases, and struggling to understand concepts such as algorithmic complexity, how networks work, and many other things that are part of most developers’ day-to-day work.
However, as useful as all that was, I still hadn’t figured out something fundamental by the end of the day:
*By a complex solution, I mean applications based on distributed systems in which several components, not necessarily connected to each other initially, need to interact in a synchronized way.
To answer these questions, I decided to build a website that could serve both as a case study and as a portfolio. And that’s how this website came to be. Well, not exactly this one, but an earlier version built as a Vue SPA, where most of the complexity came from developing the animations rather than from the architecture itself.
What happened next? I bought a domain, created a Netlify account, connected it to my GitHub repository, configured the DNS records, and that was it.
It had been straightforward: read a handful of documentation pages, watch a few YouTube videos, spend a few dozen hours building the website, and call it a day.
I didn’t have any problems with it for the next four years.
Until six months ago.
— "I’d like to share my experiences with other developers."
— Should I write LinkedIn posts?
— "Hell no!"
The obvious option, then, was to write the articles on the website I already had. What could possibly go wrong?
Of course, the requirements had changed completely.
After evaluating how well my old website could scale, I realized it was missing several fundamental elements that should have been in place long before development even began.
In short, the old website had more than just technical scalability issues. It was also tightly coupled to its implementation, difficult to reuse, and had accumulated a lot of knowledge that existed only in my head.
At that point, I realized what I needed: not simply a bigger application, but a foundation I could continue building on.
I’d be lying if I said I knew exactly how far this project would go, but the main goals were clear:
One thing to keep in mind is that, unlike corporate development, publishing personal projects comes with another consideration: the fewer subscriptions and services you have to pay for, the better. Every platform you integrate with imposes its own opinions on YOUR solution. Based on my experience, I recommend looking for open-source alternatives that help you reduce costs and retain control over the entire CI/CD process. You’ll also gain much more hands-on experience with DevOps. It’s a win-win.
So, what architecture did I go with? Here it is.
I’m not going to go into detail about why I chose these particular technologies or which other external services I use. But, as you can see, the system consists of:
Each blog post has a URL containing a language code and a slug. I designed it this way because, as we’ll see in future articles, using IDs or alphanumeric strings in URLs can hurt the SEO of publicly accessible pages. The URI structure is therefore as follows:
/en/blog/article-slug/es/blog/slug-articuloThis way, the page knows which version of the content to request. When someone opens an article, the client fetches the content from the CMS API and renders the article page. Rich content is displayed using interface components for headings, paragraphs, links, lists, blockquotes, images, and code blocks.
The same applies to the Portfolio and About pages. Instead of storing each piece of text in i18n files that would need to be version-controlled alongside the rest of the code and make it harder to manage new content, this information is managed through the CMS, with separate entries for each language.
Payload CMS is the content management system used by this application. From its admin panel, you can create and edit blog posts, manage images, and organize other site data, such as projects, categories, skills, and experience.
The text editor uses Lexical. Content isn’t stored as a manually written HTML page, but as a data structure describing its different elements: paragraphs, headings, links, lists, blockquotes, images, and code blocks. The frontend then interprets that structure and turns it into visual elements.
But Payload does more than provide an interface for editing content. It also defines the application’s data model, handles persistence, and exposes the API consumed by the frontend.
This is where PostgreSQL comes in.
The different collections we define in Payload — blog posts, projects, categories, skills, professional experience, and so on — are ultimately represented as persisted data in our relational database. Payload manages this layer and exposes an interface that allows the web application to query that data without needing to know exactly how it’s stored.
At the time of writing, the database’s entity-relationship diagram looks something like this:
The diagram helps reveal something that may be less obvious from Payload’s admin panel: behind the collections and relationships we manage through the CMS is a data model that defines how the different pieces of content relate to one another.
This separation is important from an architectural perspective. The website doesn’t need to know PostgreSQL’s internal schema; it communicates with Payload through its API, which acts as the boundary between the presentation layer and the data layer.
In return, this boundary makes the API a contract between the two applications: changes to the data, relationships, or API responses can affect the public website, even when each service is built independently.
In other words, Payload isn’t simply "where I write my articles." It’s the component that connects content management, the data model, and the API consumed by the frontend.
The deployment process starts with the GitHub monorepo and relies on a separate Docker image for each application. The Dockerfiles define how to build the frontend and the CMS, while Dokploy coordinates the builds and runs each image as a service. This means deployment doesn’t depend on manually setting up each server: the code and the process used to package it move forward together.
This workflow makes it possible to take a change from the repository to production in a matter of minutes. Before that happens, staging environments provide a validation stage similar to UAT: the deployed application is tested under conditions similar to the real environment before the change is exposed to users.
GitHub stores the code; Docker makes the build artifact reproducible; Dokploy automates the build and execution process. The result is a short delivery cycle, with a validation stage before production and clearly defined boundaries between services.
Finally, Cloudflare is a central part of the infrastructure. It manages DNS and, with the proxy enabled, public domains resolve to Cloudflare’s network rather than directly to the VPS IP address. Cloudflare receives incoming requests and forwards them to the origin server, which sits behind that layer.
This setup makes it possible to apply security rules and mitigate denial-of-service attacks before traffic reaches the VPS. It also allows responses and resources covered by the configured rules to be cached, reducing requests to the origin server and improving delivery performance. The extent of caching depends on those rules: not all traffic is necessarily cached.
Cloudflare also provides R2 for media files. Payload uploads the files to the bucket and stores their metadata and relationships in PostgreSQL, while the browser requests the files through their public URLs. This means pages are delivered through the frontend, while media files are served from R2, with Cloudflare forming part of the infrastructure responsible for receiving and delivering traffic.
To wrap things up, you don’t master architecture through theory alone. You learn by making decisions, putting them to the test, and understanding their consequences. Every project is an opportunity to expand what we think is possible and turn uncertainty into experience. As Linus Torvalds said:
"Talk is cheap. Show me the code."
Ultimately, pick a challenge, build something, and learn along the way.
Last updated: October 27, 2026