fullstack

Building aimen.dev: A Full-Stack Blog Platform

I built this with help from Claude. I wanted to get some experience with a fullstack project using tools I haven’t used before. Using Claude I'd describe what I wanted to build, get a concrete plan back, ask why every decision was made the way it was, hit an error, describe it, work through what was actually happening, and come away understanding something I didn't before.

That's the context for this post. aimen.dev is live, it's a real fullstack application, and I built it, but I didn't build it alone, and pretending otherwise would make this a worse blog.

Here's what actually went into it.

Why build it at all

I could have used Ghost. WordPress. Substack. Any number of things that would have had me writing posts within an hour.

I didn't, because the blog is the project. I wanted to build something real, not follow a tutorial, not fill in a template, but actually design a database schema, wire up an API, handle authentication, deploy to production, and debug things when they broke. The blog subject and the blog infrastructure are the same thing.

That choice made everything harder and everything more valuable.

The stack

Next.js 16 (App Router) — fullstack, frontend and API in the same repo. No separate Express server. The App Router mental model took adjusting to — server components that query databases directly, no useEffect for data fetching — but once it clicked, it made everything simpler.

PostgreSQL on Railway — managed database, free tier to start. Railway gives you a DATABASE_URL connection string. Once you understand what's in it (protocol, username, password, server address, port, database name), it stops looking cryptic.

Prisma — type-safe ORM. Define your schema in a .prisma file, run prisma db push, and your tables exist. The TypeScript client means if you try to query a field that doesn't exist, your editor tells you before you run anything.

NextAuth.js v5 — GitHub OAuth. The access control system is one environment variable: ADMIN_EMAIL. If your GitHub account email matches it, you're in. Everyone else gets rejected.

Tailwind CSS v4 — v4 configures plugins differently from v3. @tailwindcss/typography goes in globals.css via @plugin "@tailwindcss/typography", not in tailwind.config.ts. I learned this the hard way.

Vercel + Railway — Vercel for the Next.js app, Railway for the database. Both have free tiers. Total starting cost: $0.

What I built

Markdown blog posts

Posts are stored as Markdown in the database. react-markdown renders them in the browser. The Tailwind Typography plugin styles them — headings, code blocks, paragraphs, all handled.

GitHub-based admin auth

Only I can access /admin. NextAuth v5 handles the GitHub OAuth flow. Every admin API call checks the session:

const session = await auth()
if (!session) return new Response('Unauthorized', { status: 401 })

The admin area includes a post dashboard, new post editor with auto-generated slugs, edit page, and delete flow with confirmation.

Comment moderation

Readers can leave comments. Saved with approved: false by default. Admin moderation queue at /admin/comments. Only approved comments appear on the post.

Emoji reactions

Cookie-based session_id UUID, no login required. The database enforces @@unique([postId, emoji, sessionId]) — double-reacting silently does nothing. Constraint at the database level, not application code, because database constraints can't have race conditions.

Search

Debounced input on the blog listing, API route doing the Prisma query, results replacing the post list while typing.

Post navigation

Previous and next post links at the bottom of each post, queried by date.

Theme toggle

Light/dark mode with localStorage persistence and React Context.

Some of the errors I actually hit

ErrorCauseFix
MODULE_NOT_FOUND @prisma/clientprisma db push doesn't generate the TS clientnpx prisma generate — now in postinstall script
/blog returning 404Dev server not restarted after module fixRestart after any module-level fix
Unstyled post contentWrong plugin config location in Tailwind v4@plugin directive in globals.css
middleware.ts deprecatedNext.js 16 renamed itRename to proxy.ts
GitHub OAuth 404NextAuth v5 uses AUTH_GITHUB_ID not GITHUB_CLIENT_IDCorrect env var names
New post not savinggetServerSession() is NextAuth v4auth() from lib/auth.ts
Edit page 404Page didn't exist yetBuild app/admin/edit/[slug]/page.tsx
Hydration warningDark Reader browser extensionsuppressHydrationWarning on <html>
onMouseEnter in server componentEvent handlers not allowed in server componentsMove hover to CSS class in globals.css
Auth Configuration error in productionAUTH_SECRET missing from Vercel env varsAdd it, redeploy

The database schema

Four models, designed upfront before building any features:

Designing all four upfront meant no disruptive migrations mid-build.

Deployment

  1. Push to GitHub
  2. Import to Vercel
  3. Add environment variables in Vercel dashboard
  4. vercel --prod
  5. Buy domain at Porkbun (~$10/year for .dev)
  6. Add domain in Vercel → Settings → Domains
  7. Copy A record + CNAME to registrar DNS settings
  8. Update GitHub OAuth callback URL to https://aimen.dev/api/auth/callback/github
  9. Set NEXTAUTH_URL=https://aimen.dev in Vercel
  10. Redeploy

DNS propagated in about 20 minutes.

What I actually learned

Design the schema before building features. Four models upfront. No mid-build migrations.

The App Router mental model is different from Pages Router. Server components query Prisma directly. Client components need API routes. Once that's clear, the architecture makes sense.

Database constraints beat application-level checks. Race conditions can't happen at the database level.

Ask why, not just what. Every time I got code I didn't fully understand, I asked Claude to explain the reasoning. The explanations were more useful than the code. The code is temporary. The understanding stays.

Shipping is different from working. The blog worked on localhost for weeks. Making it public at a real domain is a different kind of done.

What's next

Newsletter (double opt-in) is next. After that: Terraform, Kubernetes, CI/CD, observability — the SRE transition, documented the same way this was.

Source: github.com/AimAbe/aimen-dev

Comments

No comments yet. Be the first.

Leave a comment