I wrote this post to walk through how aimen.dev works, layer by layer, using plain language. It's basically the explanation I wish I'd had before I started.
I built this with Claude's help, and some of that help was pressure testing these explanations. I'm sharing what ended up clicking for me.
Start here: what does a website actually do?
When you type aimen.dev into your browser and press enter, here's what happens in plain terms:
Your browser sends a message to a computer somewhere saying "can I have the aimen.dev homepage please?" That computer (called a server) receives the message, figures out what to send back, and sends back a bunch of text, HTML, CSS, and JavaScript, which your browser turns into the page you see.
That's it. At its core, a website is just a computer responding to requests with text.
The question is: what does the server do to figure out what to send back? For a simple site, it just sends the same file every time. For a site like aimen.dev, it does a lot more, it checks databases, handles logins, processes reactions. That's what makes it "fullstack."
What "fullstack" actually means
Frontend, the HTML, CSS, and JavaScript that your browser downloads and renders. The visual stuff. The buttons, the text, the layout.
Backend, the code that runs on a server, not in the browser. It talks to databases, checks if you're logged in, processes form submissions.
Some projects split these into two separate codebases. aimen.dev uses Next.js, which lets you build both in the same project. One codebase, one place to look when something breaks.
The pieces and what they do
Next.js, the framework
Think of a framework as a set of rules and tools for organising your code. Instead of building everything from scratch, you follow the framework's conventions and it handles a lot of the common stuff for you.
Next.js handles routing, server-side code, and building the project into files that can be deployed. The version used here (Next.js 16, App Router) means pages run on the server by default, which matters because server code can talk to a database directly.
PostgreSQL, the database
A database is just a structured way to store and retrieve data. Think of it like a very organised spreadsheet that lives on a server and can be queried with code.
PostgreSQL stores everything on aimen.dev: blog posts, comments, reactions. It lives on Railway, a hosting company that manages the database server for you.
Prisma, the translator
Databases speak SQL. Your code is TypeScript. Prisma is the layer that translates between them.
Instead of raw SQL:
SELECT title, excerpt FROM posts WHERE published = true ORDER BY created_at DESC;
You write TypeScript:
prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
select: { title: true, excerpt: true }
})
Prisma also uses a schema file, one file that defines every table, column, and relationship. Run prisma db push and the database updates to match.
TypeScript, typed JavaScript
TypeScript is JavaScript with types. A type tells TypeScript what kind of thing a variable is supposed to be. If you try to access post.titlee (typo), TypeScript catches it before you run the code. Without types, that bug only shows up at runtime, when a real user hits it.
API routes, the middlemen
Server components can talk to Prisma directly, but only at page load time. Interactions that happen after, clicking a reaction, submitting a comment, happen in the browser. The browser can't import Prisma.
API routes solve this. An API route is a server-side function with a URL. The browser sends a request, the server runs the function, sends back a response.
When you click ❤️ on a post:
- Browser sends
POST /api/reactionswith the post ID and emoji - Server saves the reaction via Prisma
- Server sends back updated counts
- Browser updates the button
The browser never touches the database directly. It only talks to the API route.
NextAuth, authentication
Authentication is how the system knows who you are and whether you're allowed to do something.
aimen.dev uses GitHub OAuth, GitHub verifies your identity, so passwords don't need to be managed. The auth is locked to one email address via ADMIN_EMAIL. Everyone else gets rejected. One environment variable is the entire access control system.
Tailwind CSS, styling
Tailwind lets you style things using short class names instead of writing CSS files. Instead of a .button CSS class, you write classes directly in your HTML. The utility classes map directly to CSS properties.
Vercel, deployment
Deployment means taking code that works on your laptop and running it on a server that's always on and reachable by anyone. Vercel connects to your GitHub repo, watches for commits, and automatically builds and deploys new versions. Secret config values live in the Vercel dashboard, never in the code.
How it all fits together
What happens when you read a blog post:
You type aimen.dev/blog/some-post
↓
Browser sends request to Vercel
↓
Vercel runs Next.js server code
↓
Server calls Prisma: find post with this slug
↓
Prisma sends SQL to PostgreSQL on Railway
↓
Railway returns the post data
↓
Server renders the page HTML
↓
Vercel sends HTML to your browser
↓
Browser displays the page
When you click a reaction:
You click ❤️
↓
Browser sends POST /api/reactions
↓
API route checks your session_id cookie
↓
Prisma tries to save the reaction
(database rejects duplicates automatically)
↓
Prisma fetches updated counts
↓
API route sends counts back to browser
↓
Browser updates the button
Two flows. Same layers. Browser → Vercel → Next.js → Prisma → Railway → back up.
The design choices that aren't obvious
Why store posts as Markdown? Markdown is plain text with formatting symbols. Storing posts as Markdown means the content is just a string in the database, simple and portable. react-markdown converts it to HTML when the page renders.
Why use a database and not just files? Files can't be queried. If you want the three most recent posts, search by title, or filter by tag, you need a database. Comments and reactions also need somewhere to live.
Why design the schema upfront? Changing a database schema later requires migrations, which can be disruptive. Designing all four tables (Post, Comment, Reaction, Subscriber) before writing any code meant no disruptive changes mid-build.
Why cookies for reactions instead of login? Requiring login to react means building a full user system, registration, passwords, profiles. That's a lot for a small feature. A UUID in a cookie is anonymous, frictionless, and the database unique constraint prevents abuse.
Why moderate comments by default? Every comment starts as approved: false. Nothing appears publicly until reviewed. For a new blog with zero traffic this costs nothing. For a blog that gets spam it prevents it from ever showing up. Starting strict and relaxing later is easier than the reverse.
The one thing I'd tell someone starting out
Architecture sounds intimidating. It's not. It's basically three questions: what are the pieces, what does each one do, and how do they talk to each other?
For aimen.dev, Next.js runs the app, Prisma talks to PostgreSQL, NextAuth handles auth, Tailwind handles styling, and Vercel runs it in production. Each piece has a clear job.
You don't need to understand all of them perfectly before you start. You just need enough understanding to make a call, then you learn the rest by building.
That's been the whole theme of this project for me.