How I Built a Free AI Prompt Library With Node.js and PostgreSQL — From Zero to Live in 30 Days A developer built PromptZyo, a free AI prompt library for professionals, using Node.js, Express, EJS, and PostgreSQL, deploying it live on Railway in 30 days. The site serves structured prompts organized by profession to help users get better results from AI tools like ChatGPT and Claude. If you have been following my Dev.to articles, you know I built Chatzyo — a real-time video chat platform using WebRTC. That project taught me a lot about real-time infrastructure, WebSocket connections, and scaling live traffic. PromptZyo is completely different. No WebSockets. No real-time anything. Just a well-structured content site built for SEO and passive income. Here is the honest technical breakdown of how I built it, the decisions I made, and what I would do differently. Most professionals — teachers, lawyers, HR managers, nurses — know that ChatGPT and Claude can help them. But they keep getting generic, useless results. The problem is not the AI tool.The problem is the prompt. A vague prompt: "write a lesson plan "Result: generic, unusable output A specific prompt: "write a 45-minute lesson plan for Grade 5 students on multiplying fractions for the first time, for a class that excels at whole number operations but has never seen fraction multiplication" Result: genuinely useful first draft Nobody had built a library of professionally structured prompts organized by profession. So I built PromptZyo https://promptzyo.com — a free AI prompt library for professionals. I know Node well from Chatzyo. For a content-heavy site with PostgreSQL queries and server-side rendering, Express is fast, lightweight, and does exactly what I need. No need for a heavy framework. Express + EJS handles the routing and templating cleanly. This was a deliberate decision.For Chatzyo I used React for the interactive UI. For PromptZyo — a content library where SEO matters more than interactivity — server-side rendering with EJS is the right choice. For a prompt library where I need 150+ pages indexed quickly — EJS wins. My initial instinct was MongoDB — I know it well and prompts seem like document data. But the relational structure changed my mind: This is relational data. PostgreSQL handles it cleanly with foreign keys and JOIN queries. MongoDB would have required manual relationship management. The query that convinced me: SELECT p. , pr.name as profession name, pr.slug as profession slug, c.name as category name FROM prompts p LEFT JOIN professions pr ON p.profession id = pr.id LEFT JOIN categories c ON p.category id = c.id WHERE pr.slug = $1 AND p.is active = true ORDER BY p.created at DESC Clean, fast, readable. PostgreSQL was the right call. For Chatzyo I use a different hosting setup. For PromptZyo I chose Railway for a few reasons: The one gotcha: Railway's internal networking only works within the same project. My PostgreSQL and Node app are in the same Railway project — so the internal connection string works. If they were in different projects, I would need the public proxy URL which has higher latency. Cloudflare sits in front of Railway and handles: Setup was straightforward — point Cloudflare nameservers at Namecheap, add CNAME record pointing to Railway's deployment URL, done. The schema has 6 main tables: -- Core content tables professions id, name, slug, description, icon, is active categories id, name, slug, profession id prompts id, title, slug, content, profession id, category id, ai tool, difficulty, copy count, is active, created at -- User features built but not launched yet users id, google id, email, name, created at saved prompts user id, prompt id, saved at -- Blog blog posts id, title, slug, excerpt, content, author, category, is published, published at, reading time -- Sessions session sid, sess, expire The copy count column tracks how many times each prompt has been copied. Every time a user clicks Copy Prompt, it fires a POST to /api/track-copy/:id which increments the count. js router.post '/track-copy/:id', async req, res = { try { await pool.query 'UPDATE prompts SET copy count = copy count + 1 WHERE id = $1', req.params.id ; res.json { success: true } ; } catch err { console.error err ; res.json { error: 'Failed to track' } ; } } ; Simple but effective engagement tracking. This is where PromptZyo differs most from Chatzyo. Chatzyo's traffic came from localized pages. PromptZyo's traffic will come from a different content structure. Each URL is a crawlable page with unique title, meta description, and canonical tag. Every page type has appropriate schema: // Profession pages { "@type": "CollectionPage", "numberOfItems": prompts.length, "breadcrumb": { ... } } // Individual prompt pages { "@type": "HowTo", // for the prompt itself "step": ... } { "@type": "FAQPage", // for the FAQ section "mainEntity": ... } // Blog posts { "@type": "Article", "author": { "@type": "Person", ... }, "datePublished": "...", "dateModified": "..." } Schema markup helps Google understand what each page is — and can trigger rich results in search. The sitemap generates dynamically from the database: js router.get '/', async req, res = { const professions = await pool.query 'SELECT slug FROM professions WHERE is active = true' ; const prompts = await pool.query 'SELECT slug FROM prompts WHERE is active = true' ; const blogPosts = await pool.query 'SELECT slug FROM blog posts WHERE is published = true' ; // Build XML dynamically let xml =