Files
biblical-guide.com/temp/bible-chat-implementation-plan.md
Claude Assistant ee99e93ec2 Implement dynamic daily verse system with rotating Biblical content
- Add daily-verse API endpoint with 7 rotating verses in Romanian and English
- Replace static homepage verse with dynamic fetch from API
- Ensure consistent daily rotation using day-of-year calculation
- Support both ro and en locales for verse content

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 19:22:34 +00:00

37 KiB

Biblical Guide Web App - Granular Implementation Plan

Primary UI Language: Romanian Secondary UI Language: English

Project Overview

A self-hosted Bible study web application with AI chat capabilities, real-time features, and comprehensive user management using PostgreSQL with extensions for all functionality.

Technology Stack

Core Technologies

  • Frontend: Next.js 14 (App Router) + TypeScript
  • Backend: Next.js API Routes + WebSocket Server
  • Database: PostgreSQL 16 with extensions (pgvector, pg_cron, pg_jwt)
  • Cache: PostgreSQL with UNLOGGED tables for caching
  • Real-time: Socket.io for WebSocket connections
  • AI/LLM: Azure OpenAI API + Ollama API
  • Containerization: Docker + Docker Compose
  • Testing: Jest + React Testing Library
  • Styling: Tailwind CSS + shadcn/ui components

Project Structure


├── docker/
│   ├── Dockerfile.dev
│   ├── Dockerfile.prod
│   └── nginx/
│       └── nginx.conf
├── docker-compose.yml
├── docker-compose.prod.yml
├── src/
│   ├── app/
│   │   ├── (auth)/
│   │   ├── (main)/
│   │   ├── api/
│   │   └── layout.tsx
│   ├── components/
│   ├── lib/
│   ├── hooks/
│   ├── types/
│   ├── utils/
│   └── middleware.ts
├── prisma/
│   ├── schema.prisma
│   ├── migrations/
│   └── seed/
├── scripts/
│   ├── import-bible.ts
│   └── setup-db.sh
├── tests/
├── public/
└── config/

Phase 1: Foundation Setup (Days 1-3)

Step 1.1: Initialize Project and Docker Environment

# Create project directory
mkdir bible-chat-app && cd bible-chat-app

# Initialize Next.js with TypeScript
npx create-next-app@latest . --typescript --tailwind --app --no-src-dir

# Create Docker configuration

Docker Compose Configuration (docker-compose.yml):

version: '3.8'
services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: bible_chat
      POSTGRES_USER: bible_admin
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
  
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
      - "3001:3001"  # WebSocket port
    depends_on:
      - postgres
    environment:
      DATABASE_URL: postgresql://bible_admin:${DB_PASSWORD}@postgres:5432/bible_chat
      AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY}
      OLLAMA_API_URL: ${OLLAMA_API_URL}

Step 1.2: Database Schema Setup

Install Dependencies:

npm install @prisma/client prisma @types/node
npm install -D @types/bcryptjs bcryptjs

Prisma Schema (prisma/schema.prisma):

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id              String    @id @default(uuid())
  email           String    @unique
  passwordHash    String
  name            String?
  theme           String    @default("light")
  fontSize        String    @default("medium")
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt
  lastLoginAt     DateTime?
  
  sessions        Session[]
  bookmarks       Bookmark[]
  notes           Note[]
  chatMessages    ChatMessage[]
  prayerRequests  PrayerRequest[]
  readingHistory  ReadingHistory[]
  preferences     UserPreference[]
}

model Session {
  id          String   @id @default(uuid())
  userId      String
  token       String   @unique
  expiresAt   DateTime
  createdAt   DateTime @default(now())
  
  user        User     @relation(fields: [userId], references: [id])
  
  @@index([userId])
  @@index([token])
}

model BibleBook {
  id          Int      @id
  name        String
  testament   String
  orderNum    Int
  chapters    BibleChapter[]
  
  @@index([testament])
}

model BibleChapter {
  id          String   @id @default(uuid())
  bookId      Int
  chapterNum  Int
  verses      BibleVerse[]
  
  book        BibleBook @relation(fields: [bookId], references: [id])
  
  @@unique([bookId, chapterNum])
  @@index([bookId])
}

model BibleVerse {
  id          String   @id @default(uuid())
  chapterId   String
  verseNum    Int
  text        String   @db.Text
  version     String   @default("KJV")
  
  chapter     BibleChapter @relation(fields: [chapterId], references: [id])
  bookmarks   Bookmark[]
  notes       Note[]
  
  @@unique([chapterId, verseNum, version])
  @@index([chapterId])
  @@index([version])
}

model ChatMessage {
  id          String   @id @default(uuid())
  userId      String
  role        String   // 'user' or 'assistant'
  content     String   @db.Text
  metadata    Json?    // Store verse references, etc.
  createdAt   DateTime @default(now())
  
  user        User     @relation(fields: [userId], references: [id])
  
  @@index([userId, createdAt])
}

model Bookmark {
  id          String   @id @default(uuid())
  userId      String
  verseId     String
  note        String?
  color       String   @default("#FFD700")
  createdAt   DateTime @default(now())
  
  user        User     @relation(fields: [userId], references: [id])
  verse       BibleVerse @relation(fields: [verseId], references: [id])
  
  @@unique([userId, verseId])
  @@index([userId])
}

model PrayerRequest {
  id          String   @id @default(uuid())
  userId      String?
  content     String   @db.Text
  isAnonymous Boolean  @default(true)
  prayerCount Int      @default(0)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  
  user        User?    @relation(fields: [userId], references: [id])
  prayers     Prayer[]
  
  @@index([createdAt])
}

model Prayer {
  id            String   @id @default(uuid())
  requestId     String
  ipAddress     String   // For anonymous prayer counting
  createdAt     DateTime @default(now())
  
  request       PrayerRequest @relation(fields: [requestId], references: [id])
  
  @@unique([requestId, ipAddress])
}

Step 1.3: Database Extensions and Functions

Database Initialization Script (scripts/init.sql):

-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "vector";

-- Create cache table for Bible verses
CREATE UNLOGGED TABLE verse_cache (
  key VARCHAR(255) PRIMARY KEY,
  value TEXT,
  expires_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Create index for full-text search
CREATE INDEX verse_text_gin_idx ON "BibleVerse" USING gin(to_tsvector('english', text));

-- Function for verse search
CREATE OR REPLACE FUNCTION search_verses(search_query TEXT, limit_count INT DEFAULT 10)
RETURNS TABLE(
  verse_id UUID,
  book_name TEXT,
  chapter_num INT,
  verse_num INT,
  verse_text TEXT,
  rank REAL
) AS $$
BEGIN
  RETURN QUERY
  SELECT 
    v.id,
    b.name,
    c."chapterNum",
    v."verseNum",
    v.text,
    ts_rank(to_tsvector('english', v.text), plainto_tsquery('english', search_query)) as rank
  FROM "BibleVerse" v
  JOIN "BibleChapter" c ON v."chapterId" = c.id
  JOIN "BibleBook" b ON c."bookId" = b.id
  WHERE to_tsvector('english', v.text) @@ plainto_tsquery('english', search_query)
  ORDER BY rank DESC
  LIMIT limit_count;
END;
$$ LANGUAGE plpgsql;

-- Session cleanup function
CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
RETURNS void AS $$
BEGIN
  DELETE FROM "Session" WHERE "expiresAt" < NOW();
END;
$$ LANGUAGE plpgsql;

Phase 2: Core Backend Implementation (Days 4-8)

Step 2.1: Environment Configuration

.env.local file:

DATABASE_URL=postgresql://bible_admin:password@localhost:5432/bible_chat
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=generate-random-secret-here
AZURE_OPENAI_KEY=your-azure-key
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_DEPLOYMENT=gpt-4
OLLAMA_API_URL=http://your-ollama-server:11434
JWT_SECRET=your-jwt-secret
REDIS_URL=redis://localhost:6379

Step 2.2: Database Connection and Utilities

src/lib/db.ts:

import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

Step 2.3: Authentication System

src/lib/auth/index.ts:

import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { prisma } from '@/lib/db'

export async function createUser(email: string, password: string, name?: string) {
  const passwordHash = await bcrypt.hash(password, 10)
  return prisma.user.create({
    data: { email, passwordHash, name }
  })
}

export async function validateUser(email: string, password: string) {
  const user = await prisma.user.findUnique({ where: { email } })
  if (!user) return null
  
  const isValid = await bcrypt.compare(password, user.passwordHash)
  return isValid ? user : null
}

export function generateToken(userId: string): string {
  return jwt.sign({ userId }, process.env.JWT_SECRET!, { expiresIn: '7d' })
}

Step 2.4: Bible Data Import Script

scripts/import-bible.ts:

import { PrismaClient } from '@prisma/client'
import fs from 'fs/promises'
import path from 'path'

const prisma = new PrismaClient()

interface BibleData {
  books: Array<{
    id: number
    name: string
    testament: string
    chapters: Array<{
      number: number
      verses: Array<{
        number: number
        text: string
      }>
    }>
  }>
}

async function importBible() {
  const dataPath = path.join(process.cwd(), 'data', 'bible-kjv.json')
  const bibleData: BibleData = JSON.parse(await fs.readFile(dataPath, 'utf-8'))
  
  for (const book of bibleData.books) {
    // Create book
    await prisma.bibleBook.create({
      data: {
        id: book.id,
        name: book.name,
        testament: book.testament,
        orderNum: book.id
      }
    })
    
    // Create chapters and verses
    for (const chapter of book.chapters) {
      const createdChapter = await prisma.bibleChapter.create({
        data: {
          bookId: book.id,
          chapterNum: chapter.number
        }
      })
      
      // Bulk create verses
      await prisma.bibleVerse.createMany({
        data: chapter.verses.map(verse => ({
          chapterId: createdChapter.id,
          verseNum: verse.number,
          text: verse.text,
          version: 'KJV'
        }))
      })
    }
  }
}

importBible()
  .then(() => console.log('Bible import completed'))
  .catch(console.error)
  .finally(() => prisma.$disconnect())

Phase 3: API Routes Implementation (Days 9-12)

Step 3.1: Authentication API Routes

src/app/api/auth/register/route.ts:

import { NextResponse } from 'next/server'
import { createUser, generateToken } from '@/lib/auth'
import { prisma } from '@/lib/db'

export async function POST(request: Request) {
  try {
    const { email, password, name } = await request.json()
    
    // Validation
    if (!email || !password) {
      return NextResponse.json({ error: 'Email and password required' }, { status: 400 })
    }
    
    // Check if user exists
    const existing = await prisma.user.findUnique({ where: { email } })
    if (existing) {
      return NextResponse.json({ error: 'User already exists' }, { status: 409 })
    }
    
    // Create user
    const user = await createUser(email, password, name)
    const token = generateToken(user.id)
    
    // Create session
    await prisma.session.create({
      data: {
        userId: user.id,
        token,
        expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
      }
    })
    
    return NextResponse.json({ user: { id: user.id, email: user.email, name: user.name }, token })
  } catch (error) {
    return NextResponse.json({ error: 'Server error' }, { status: 500 })
  }
}

Step 3.2: Bible API Routes

src/app/api/bible/search/route.ts:

import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const query = searchParams.get('q')
  const limit = parseInt(searchParams.get('limit') || '10')
  
  if (!query) {
    return NextResponse.json({ error: 'Query required' }, { status: 400 })
  }
  
  // Use raw SQL for full-text search
  const results = await prisma.$queryRaw`
    SELECT * FROM search_verses(${query}, ${limit})
  `
  
  return NextResponse.json({ results })
}

Step 3.3: AI Chat API Integration

src/lib/ai/azure-openai.ts:

import { AzureOpenAI } from 'openai'

const client = new AzureOpenAI({
  apiKey: process.env.AZURE_OPENAI_KEY!,
  apiVersion: '2024-02-01',
  endpoint: process.env.AZURE_OPENAI_ENDPOINT!,
})

export async function generateChatResponse(
  messages: Array<{ role: string; content: string }>,
  verseContext?: string
) {
  const systemPrompt = `You are a helpful Bible study assistant. Always provide scripture references for your answers. ${verseContext ? `Context: ${verseContext}` : ''}`
  
  const response = await client.chat.completions.create({
    model: process.env.AZURE_OPENAI_DEPLOYMENT!,
    messages: [
      { role: 'system', content: systemPrompt },
      ...messages
    ],
    temperature: 0.7,
    max_tokens: 1000
  })
  
  return response.choices[0].message.content
}

src/lib/ai/embeddings.ts:

export async function generateEmbedding(text: string): Promise<number[]> {
  const response = await fetch(`${process.env.OLLAMA_API_URL}/api/embeddings`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'nomic-embed-text',
      prompt: text
    })
  })
  
  const data = await response.json()
  return data.embedding
}

Step 3.4: WebSocket Server for Real-time Features

src/lib/websocket/server.ts:

import { Server } from 'socket.io'
import { createServer } from 'http'
import { parse } from 'url'
import next from 'next'

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = createServer((req, res) => {
    const parsedUrl = parse(req.url!, true)
    handle(req, res, parsedUrl)
  })
  
  const io = new Server(server, {
    cors: {
      origin: process.env.NEXTAUTH_URL,
      methods: ['GET', 'POST']
    }
  })
  
  io.on('connection', (socket) => {
    console.log('Client connected:', socket.id)
    
    // Join prayer room
    socket.on('join-prayer-room', () => {
      socket.join('prayers')
    })
    
    // Handle new prayer
    socket.on('new-prayer', async (data) => {
      // Save to database
      // Broadcast to all in prayer room
      io.to('prayers').emit('prayer-added', data)
    })
    
    // Handle prayer count update
    socket.on('pray-for', async (requestId) => {
      // Update count in database
      // Broadcast updated count
      io.to('prayers').emit('prayer-count-updated', { requestId, count: newCount })
    })
    
    socket.on('disconnect', () => {
      console.log('Client disconnected:', socket.id)
    })
  })
  
  server.listen(3001, () => {
    console.log('WebSocket server running on port 3001')
  })
})

Phase 4: Frontend Implementation (Days 13-18)

Primary UI Language: Romanian Secondary UI Language: English

Step 4.1: Component Library Setup

Install UI Dependencies:

npm install @radix-ui/react-dropdown-menu @radix-ui/react-dialog
npm install @radix-ui/react-tabs @radix-ui/react-toast
npm install socket.io-client zustand
npm install react-markdown remark-gfm

Step 4.2: Global State Management

src/lib/store/index.ts:

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface AppState {
  user: User | null
  theme: 'light' | 'dark'
  fontSize: 'small' | 'medium' | 'large'
  currentBook: number
  currentChapter: number
  bookmarks: Bookmark[]
  setUser: (user: User | null) => void
  setTheme: (theme: 'light' | 'dark') => void
  setFontSize: (size: 'small' | 'medium' | 'large') => void
}

export const useStore = create<AppState>()(
  persist(
    (set) => ({
      user: null,
      theme: 'light',
      fontSize: 'medium',
      currentBook: 1,
      currentChapter: 1,
      bookmarks: [],
      setUser: (user) => set({ user }),
      setTheme: (theme) => set({ theme }),
      setFontSize: (fontSize) => set({ fontSize }),
    }),
    {
      name: 'bible-chat-storage',
    }
  )
)

Step 4.3: Main Layout Component

src/app/layout.tsx:

import './globals.css'
import { Providers } from '@/components/providers'
import { Navigation } from '@/components/navigation'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>
          <Navigation />
          <main className="container mx-auto px-4 py-8">
            {children}
          </main>
        </Providers>
      </body>
    </html>
  )
}

Step 4.4: Bible Reader Component

src/components/bible/reader.tsx:

'use client'

import { useState, useEffect } from 'react'
import { useStore } from '@/lib/store'

export function BibleReader() {
  const { currentBook, currentChapter } = useStore()
  const [verses, setVerses] = useState([])
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    fetchChapter(currentBook, currentChapter)
  }, [currentBook, currentChapter])
  
  async function fetchChapter(bookId: number, chapterNum: number) {
    setLoading(true)
    const res = await fetch(`/api/bible/chapter?book=${bookId}&chapter=${chapterNum}`)
    const data = await res.json()
    setVerses(data.verses)
    setLoading(false)
  }
  
  const handleVerseClick = async (verseId: string) => {
    // Add to bookmarks
    await fetch('/api/bookmarks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ verseId })
    })
  }
  
  if (loading) return <div>Loading...</div>
  
  return (
    <div className="prose prose-lg max-w-none">
      {verses.map((verse: any) => (
        <span
          key={verse.id}
          className="verse hover:bg-yellow-100 cursor-pointer"
          onClick={() => handleVerseClick(verse.id)}
        >
          <sup className="text-xs mr-1">{verse.verseNum}</sup>
          {verse.text}
        </span>
      ))}
    </div>
  )
}

Step 4.5: AI Chat Interface

src/components/chat/chat-interface.tsx:

'use client'

import { useState, useRef, useEffect } from 'react'
import { Send } from 'lucide-react'

export function ChatInterface() {
  const [messages, setMessages] = useState([])
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(false)
  const messagesEndRef = useRef(null)
  
  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
  }
  
  useEffect(scrollToBottom, [messages])
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!input.trim() || loading) return
    
    const userMessage = { role: 'user', content: input }
    setMessages(prev => [...prev, userMessage])
    setInput('')
    setLoading(true)
    
    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
          messages: [...messages, userMessage] 
        })
      })
      
      const data = await res.json()
      setMessages(prev => [...prev, { 
        role: 'assistant', 
        content: data.response 
      }])
    } catch (error) {
      console.error('Chat error:', error)
    } finally {
      setLoading(false)
    }
  }
  
  return (
    <div className="flex flex-col h-[600px] border rounded-lg">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((msg, idx) => (
          <div
            key={idx}
            className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
          >
            <div
              className={`max-w-[70%] p-3 rounded-lg ${
                msg.role === 'user' 
                  ? 'bg-blue-500 text-white' 
                  : 'bg-gray-100'
              }`}
            >
              {msg.content}
            </div>
          </div>
        ))}
        {loading && (
          <div className="flex justify-start">
            <div className="bg-gray-100 p-3 rounded-lg">
              <div className="flex space-x-2">
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" />
                <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" />
              </div>
            </div>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>
      
      <form onSubmit={handleSubmit} className="p-4 border-t">
        <div className="flex space-x-2">
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Ask about the Bible..."
            className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2"
            disabled={loading}
          />
          <button
            type="submit"
            disabled={loading || !input.trim()}
            className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
          >
            <Send className="w-5 h-5" />
          </button>
        </div>
      </form>
    </div>
  )
}

Step 4.6: Real-time Prayer Wall

src/components/prayer/prayer-wall.tsx:

'use client'

import { useEffect, useState } from 'react'
import { io, Socket } from 'socket.io-client'

export function PrayerWall() {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [prayers, setPrayers] = useState([])
  const [newPrayer, setNewPrayer] = useState('')
  
  useEffect(() => {
    const socketInstance = io('http://localhost:3001')
    setSocket(socketInstance)
    
    socketInstance.emit('join-prayer-room')
    
    socketInstance.on('prayer-added', (prayer) => {
      setPrayers(prev => [prayer, ...prev])
    })
    
    socketInstance.on('prayer-count-updated', ({ requestId, count }) => {
      setPrayers(prev => prev.map(p => 
        p.id === requestId ? { ...p, prayerCount: count } : p
      ))
    })
    
    // Load initial prayers
    fetchPrayers()
    
    return () => {
      socketInstance.disconnect()
    }
  }, [])
  
  const fetchPrayers = async () => {
    const res = await fetch('/api/prayers')
    const data = await res.json()
    setPrayers(data.prayers)
  }
  
  const handleSubmitPrayer = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!newPrayer.trim()) return
    
    const res = await fetch('/api/prayers', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: newPrayer, isAnonymous: true })
    })
    
    const prayer = await res.json()
    socket?.emit('new-prayer', prayer)
    setNewPrayer('')
  }
  
  const handlePrayFor = (requestId: string) => {
    socket?.emit('pray-for', requestId)
  }
  
  return (
    <div className="space-y-6">
      <form onSubmit={handleSubmitPrayer} className="space-y-4">
        <textarea
          value={newPrayer}
          onChange={(e) => setNewPrayer(e.target.value)}
          placeholder="Share your prayer request..."
          className="w-full p-3 border rounded-lg resize-none h-24"
        />
        <button
          type="submit"
          className="px-6 py-2 bg-purple-600 text-white rounded-lg"
        >
          Submit Prayer Request
        </button>
      </form>
      
      <div className="space-y-4">
        {prayers.map((prayer: any) => (
          <div key={prayer.id} className="p-4 bg-white rounded-lg shadow">
            <p className="mb-3">{prayer.content}</p>
            <div className="flex items-center justify-between">
              <span className="text-sm text-gray-500">
                {new Date(prayer.createdAt).toLocaleDateString()}
              </span>
              <button
                onClick={() => handlePrayFor(prayer.id)}
                className="flex items-center space-x-2 px-4 py-2 bg-purple-100 rounded-lg hover:bg-purple-200"
              >
                <span>🙏</span>
                <span>{prayer.prayerCount} prayers</span>
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

Phase 5: Testing Implementation (Days 19-20)

Step 5.1: Unit Tests Setup

jest.config.js:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  testMatch: ['**/__tests__/**/*.test.ts', '**/__tests__/**/*.test.tsx'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
  ],
}

Step 5.2: API Route Tests

tests/api/auth.test.ts:

import { createMocks } from 'node-mocks-http'
import { POST as registerHandler } from '@/app/api/auth/register/route'

describe('/api/auth/register', () => {
  test('creates new user successfully', async () => {
    const { req, res } = createMocks({
      method: 'POST',
      body: {
        email: 'test@example.com',
        password: 'SecurePass123!',
        name: 'Test User'
      },
    })
    
    await registerHandler(req as any)
    
    expect(res._getStatusCode()).toBe(200)
    const json = JSON.parse(res._getData())
    expect(json).toHaveProperty('token')
    expect(json.user.email).toBe('test@example.com')
  })
  
  test('rejects duplicate email', async () => {
    // First registration
    const { req: req1 } = createMocks({
      method: 'POST',
      body: {
        email: 'duplicate@example.com',
        password: 'Pass123!',
      },
    })
    await registerHandler(req1 as any)
    
    // Duplicate attempt
    const { req: req2, res: res2 } = createMocks({
      method: 'POST',
      body: {
        email: 'duplicate@example.com',
        password: 'Pass123!',
      },
    })
    
    await registerHandler(req2 as any)
    expect(res2._getStatusCode()).toBe(409)
  })
})

Step 5.3: Component Tests

tests/components/bible-reader.test.tsx:

import { render, screen, waitFor } from '@testing-library/react'
import { BibleReader } from '@/components/bible/reader'

jest.mock('@/lib/store', () => ({
  useStore: () => ({
    currentBook: 1,
    currentChapter: 1,
  }),
}))

describe('BibleReader', () => {
  test('renders verses correctly', async () => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({
          verses: [
            { id: '1', verseNum: 1, text: 'In the beginning...' },
            { id: '2', verseNum: 2, text: 'And the earth was...' },
          ],
        }),
      })
    ) as jest.Mock
    
    render(<BibleReader />)
    
    await waitFor(() => {
      expect(screen.getByText(/In the beginning/)).toBeInTheDocument()
    })
  })
})

Phase 6: Deployment Configuration (Days 21-22)

Step 6.1: Production Docker Configuration

docker/Dockerfile.prod:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

Step 6.2: Nginx Configuration

docker/nginx/nginx.conf:

upstream app {
    server app:3000;
}

upstream websocket {
    server app:3001;
}

server {
    listen 80;
    server_name _;
    
    client_max_body_size 10M;
    
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location /socket.io/ {
        proxy_pass http://websocket;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Step 6.3: Production Docker Compose

docker-compose.prod.yml:

version: '3.8'

services:
  postgres:
    image: pgvector/pgvector:pg16
    restart: always
    environment:
      POSTGRES_DB: bible_chat
      POSTGRES_USER: bible_admin
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - bible_network
  
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile.prod
    restart: always
    environment:
      DATABASE_URL: postgresql://bible_admin:${DB_PASSWORD}@postgres:5432/bible_chat
      AZURE_OPENAI_KEY: ${AZURE_OPENAI_KEY}
      AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT}
      AZURE_OPENAI_DEPLOYMENT: ${AZURE_OPENAI_DEPLOYMENT}
      OLLAMA_API_URL: ${OLLAMA_API_URL}
      JWT_SECRET: ${JWT_SECRET}
      NEXTAUTH_URL: ${NEXTAUTH_URL}
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
    depends_on:
      - postgres
    networks:
      - bible_network
  
  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    depends_on:
      - app
    networks:
      - bible_network

networks:
  bible_network:
    driver: bridge

volumes:
  postgres_data:

Phase 7: Performance Optimization (Days 23-24)

Step 7.1: Database Optimization

scripts/optimize-db.sql:

-- Create materialized view for popular verses
CREATE MATERIALIZED VIEW popular_verses AS
SELECT 
  v.id,
  v.text,
  b.name as book_name,
  c."chapterNum",
  v."verseNum",
  COUNT(bm.id) as bookmark_count
FROM "BibleVerse" v
JOIN "BibleChapter" c ON v."chapterId" = c.id
JOIN "BibleBook" b ON c."bookId" = b.id
LEFT JOIN "Bookmark" bm ON v.id = bm."verseId"
GROUP BY v.id, v.text, b.name, c."chapterNum", v."verseNum"
ORDER BY bookmark_count DESC
LIMIT 100;

-- Refresh materialized view periodically
CREATE OR REPLACE FUNCTION refresh_popular_verses()
RETURNS void AS $$
BEGIN
  REFRESH MATERIALIZED VIEW CONCURRENTLY popular_verses;
END;
$$ LANGUAGE plpgsql;

-- Create indexes for performance
CREATE INDEX idx_chat_messages_user_created ON "ChatMessage"("userId", "createdAt" DESC);
CREATE INDEX idx_bookmarks_user ON "Bookmark"("userId");
CREATE INDEX idx_reading_history_user ON "ReadingHistory"("userId", "viewedAt" DESC);

-- Partition chat messages by month for better performance
CREATE TABLE "ChatMessage_2024_01" PARTITION OF "ChatMessage"
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

Step 7.2: Caching Layer

src/lib/cache/index.ts:

import { prisma } from '@/lib/db'

export class CacheManager {
  static async get(key: string): Promise<string | null> {
    const result = await prisma.$queryRaw<{ value: string }[]>`
      SELECT value FROM verse_cache 
      WHERE key = ${key} 
      AND expires_at > NOW()
      LIMIT 1
    `
    return result[0]?.value || null
  }
  
  static async set(key: string, value: string, ttl: number = 3600) {
    const expiresAt = new Date(Date.now() + ttl * 1000)
    await prisma.$executeRaw`
      INSERT INTO verse_cache (key, value, expires_at)
      VALUES (${key}, ${value}, ${expiresAt})
      ON CONFLICT (key) DO UPDATE
      SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at
    `
  }
  
  static async invalidate(pattern: string) {
    await prisma.$executeRaw`
      DELETE FROM verse_cache WHERE key LIKE ${pattern}
    `
  }
}

Phase 8: Security Implementation (Day 25)

Step 8.1: Security Middleware

src/middleware.ts:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyToken } from '@/lib/auth'

export async function middleware(request: NextRequest) {
  // Rate limiting
  const ip = request.ip || 'unknown'
  const rateLimit = await checkRateLimit(ip)
  
  if (!rateLimit.allowed) {
    return new NextResponse('Too Many Requests', { status: 429 })
  }
  
  // Protected routes
  if (request.nextUrl.pathname.startsWith('/api/protected')) {
    const token = request.headers.get('authorization')?.replace('Bearer ', '')
    
    if (!token) {
      return new NextResponse('Unauthorized', { status: 401 })
    }
    
    try {
      const payload = await verifyToken(token)
      // Add user to headers
      const requestHeaders = new Headers(request.headers)
      requestHeaders.set('x-user-id', payload.userId)
      
      return NextResponse.next({
        request: {
          headers: requestHeaders,
        },
      })
    } catch (error) {
      return new NextResponse('Invalid token', { status: 401 })
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/api/:path*'],
}

async function checkRateLimit(ip: string): Promise<{ allowed: boolean }> {
  // Implement rate limiting logic using database
  // Store request counts in database with TTL
  return { allowed: true }
}

Step 8.2: Input Validation

src/lib/validation/index.ts:

import { z } from 'zod'

export const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8).regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
  name: z.string().min(2).max(50).optional(),
})

export const chatMessageSchema = z.object({
  content: z.string().min(1).max(1000),
  verseContext: z.string().optional(),
})

export const prayerRequestSchema = z.object({
  content: z.string().min(10).max(500),
  isAnonymous: z.boolean().default(true),
})

Deployment Instructions

Step 1: Server Setup

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Install Docker Compose
sudo apt install docker-compose -y

# Setup firewall
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable

Step 2: Deploy Application

# Clone repository
git clone https://github.com/yourusername/bible-chat-app.git
cd bible-chat-app

# Create .env.production file
cp .env.example .env.production
# Edit with your production values

# Build and start
docker-compose -f docker-compose.prod.yml up -d

# Run database migrations
docker-compose -f docker-compose.prod.yml exec app npx prisma migrate deploy

# Import Bible data
docker-compose -f docker-compose.prod.yml exec app npm run import-bible

Step 3: SSL Setup (Optional)

# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Get SSL certificate
sudo certbot --nginx -d yourdomain.com

Monitoring and Maintenance

Health Check Endpoint

src/app/api/health/route.ts:

export async function GET() {
  const checks = {
    database: await checkDatabase(),
    cache: await checkCache(),
    ai: await checkAIService(),
  }
  
  const healthy = Object.values(checks).every(v => v)
  
  return NextResponse.json(
    { status: healthy ? 'healthy' : 'unhealthy', checks },
    { status: healthy ? 200 : 503 }
  )
}

Backup Script

#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
docker-compose -f docker-compose.prod.yml exec -T postgres pg_dump -U bible_admin bible_chat > backup_$DATE.sql
# Upload to S3 or other storage

Performance Benchmarks

  • Page Load: < 2 seconds
  • API Response: < 500ms (cached), < 2s (database)
  • AI Chat Response: < 5 seconds
  • WebSocket Latency: < 100ms
  • Database Queries: < 50ms (with indexes)

Scalability Considerations

  1. Horizontal Scaling: Use Docker Swarm or Kubernetes
  2. Database Replication: PostgreSQL streaming replication
  3. CDN: CloudFlare for static assets
  4. Load Balancing: HAProxy or Nginx upstream
  5. Caching: Redis cluster for session storage
  6. Message Queue: RabbitMQ for background jobs

Security Checklist

  • Environment variables secured
  • HTTPS enforced
  • Rate limiting implemented
  • Input validation on all endpoints
  • SQL injection prevention (Prisma)
  • XSS protection headers
  • CSRF tokens
  • Regular dependency updates
  • Database backups configured
  • Monitoring alerts setup

Final Notes

This implementation plan provides a production-ready, scalable Biblical Guide application using only open-source technologies. The architecture is designed to handle 10,000+ concurrent users with proper scaling. Regular maintenance includes:

  1. Weekly database backups
  2. Monthly security updates
  3. Performance monitoring
  4. User feedback implementation
  5. AI model fine-tuning based on usage patterns

The modular structure allows for easy feature additions and modifications as the application grows.