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
| Error | Cause | Fix |
|---|---|---|
MODULE_NOT_FOUND @prisma/client | prisma db push doesn't generate the TS client | npx prisma generate — now in postinstall script |
/blog returning 404 | Dev server not restarted after module fix | Restart after any module-level fix |
| Unstyled post content | Wrong plugin config location in Tailwind v4 | @plugin directive in globals.css |
middleware.ts deprecated | Next.js 16 renamed it | Rename to proxy.ts |
| GitHub OAuth 404 | NextAuth v5 uses AUTH_GITHUB_ID not GITHUB_CLIENT_ID | Correct env var names |
| New post not saving | getServerSession() is NextAuth v4 | auth() from lib/auth.ts |
| Edit page 404 | Page didn't exist yet | Build app/admin/edit/[slug]/page.tsx |
| Hydration warning | Dark Reader browser extension | suppressHydrationWarning on <html> |
onMouseEnter in server component | Event handlers not allowed in server components | Move hover to CSS class in globals.css |
Auth Configuration error in production | AUTH_SECRET missing from Vercel env vars | Add it, redeploy |
The database schema
Four models, designed upfront before building any features:
- Post — slug, title, excerpt, content (Markdown), tag,
publishedboolean - Comment —
approveddefaults tofalse, moderation queue from day one - Reaction —
@@unique([postId, emoji, sessionId])at the database level - Subscriber — ready for a future newsletter
Designing all four upfront meant no disruptive migrations mid-build.
Deployment
- Push to GitHub
- Import to Vercel
- Add environment variables in Vercel dashboard
vercel --prod- Buy domain at Porkbun (~$10/year for
.dev) - Add domain in Vercel → Settings → Domains
- Copy A record + CNAME to registrar DNS settings
- Update GitHub OAuth callback URL to
https://aimen.dev/api/auth/callback/github - Set
NEXTAUTH_URL=https://aimen.devin Vercel - 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