09 / 11
AI / Web App
Prompt-O-Phobia
A community home for AI prompts that would otherwise live in a dozen scattered notes apps.
- ROLE
- Full-stack developer, solo project
- BUILT FOR
- Personal/portfolio project, open-sourced
- STACK
- Next.js 15 (App Router), React 19, Tailwind CSS 3, Prisma 6 + PostgreSQL 16, Clerk (@clerk/nextjs 7) for auth, Docker + Docker Compose
- PLATFORM
- Web app, containerized with Docker Compose for one-command local runs; deployable to Vercel + managed Postgres

THE PROBLEM
Good prompts are easy to write and easy to lose — they end up buried in chat histories, notes apps, or Slack messages, with no shared place to find one someone else already perfected. Prompt-O-Phobia gives prompt engineers, writers, and AI tinkerers a single searchable feed instead.
The design brief is simple on purpose: no gatekeeping to read (the feed is public), a low-friction way to contribute (sign in once, post a prompt with a tag), and ownership rules enforced where it actually matters — the server, not just by hiding buttons in the UI.
THE IDEA
Treat prompts like a lightweight, taggable social feed: Clerk handles identity so the app never touches passwords, and a thin Postgres schema (User + Prompt) is the only state the app owns.
Every write path resolves the acting user from the Clerk session server-side, so the client can never spoof a creatorId.
WHAT IT DOES
01
Public feed with live search
Every published prompt renders as a card with creator, content, and tag. Search is debounced and filters case-insensitively across tag, username, and prompt content — no account needed to browse.

02
Tag-based discovery and one-click copy
Clicking a tag filters the feed to that tag. Each card has a copy-to-clipboard button with visual confirmation, aimed at the actual use case — grab a prompt and paste it elsewhere.

03
Auth-gated authoring with a live preview
Users sign in via Clerk, and on a user's first authenticated write, a matching Postgres User row is provisioned keyed by their Clerk id — there's no separate registration step. The create/edit form shares one component and shows a live preview of exactly how the prompt card will appear in the feed.

HOW IT WORKS
Browser
│
├─ GET / , /profile/[id] (public, no auth)
├─ GET /api/prompt, /api/users/:id/posts
│
▼
middleware.js (clerkMiddleware)
│ protects only: /create-prompt, /update-prompt, /profile, /api/prompt/new, /api/users/me/*
▼
Next.js 15 App Router
├─ Server/Client Components (Feed, Form, Nav, Profile)
└─ Route Handlers (app/api/prompt/*, app/api/users/*)
│ auth() / currentUser() → Clerk
│ getOrCreateUser() → sync Clerk session to Postgres User
▼
Prisma Client (singleton, cached on globalThis in dev)
▼
PostgreSQL 16 (User ──< Prompt, cascade delete)
Docker Compose: app (Next.js) + db (Postgres)
container start → prisma migrate deploy → next startMiddleware protects routes, not pages
middleware.js only wraps the authoring surface (/create-prompt, /update-prompt, /profile, and the */me/* APIs) in isProtectedRoute, so the feed and other users' profiles stay crawlable/public without extra branching in the pages themselves.
Ownership is checked twice, cheaply
Middleware blocks unauthenticated requests early, but the PATCH/DELETE handlers for /api/prompt/:id still re-resolve the creator from the session and compare it to the prompt's creatorId — defense in depth without any extra infrastructure.
Prisma client is a cached singleton
The standard Next.js fix for dev-mode hot-reload exhausting Postgres connections.
Docker build bakes in NEXT_PUBLIC_* Clerk keys as build args
Since Next.js inlines them at build time — the Dockerfile comments call this out explicitly, which suggests it was a real gotcha during setup.
CHALLENGES AND WHAT I LEARNED
Mapping Clerk identity onto a separate Postgres User record
Rather than trusting Clerk as the only source of truth, this meant writing a getOrCreateUser sync path and deciding exactly which fields to mirror (clerkId, email, username, image).
Next.js 15's shift to async params
This broke the usual synchronous pattern in dynamic routes and client pages using useSearchParams — the exact Suspense boundary fix needed for update-prompt was a real issue hit during development, not a hypothetical.
Deciding how much of the auth boundary belongs in middleware
Public GETs on /api/prompt/:id needed to stay reachable while PATCH/DELETE on the same route needed ownership checks, so the enforcement couldn't live in middleware alone.
RESULTS
Full prompt lifecycle (create/read/update/delete) with server-side ownership enforcement, not just UI-level hiding of edit/delete buttons.
Clerk-based auth with zero custom password/session handling.
One-command local environment via docker compose up --build, including automatic migration deploy on container start.
STACK
LINKS

NEXT PROJECT
Moviebase
05 / CONTACT