- 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>
1482 lines
37 KiB
Markdown
1482 lines
37 KiB
Markdown
# 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
|
|
```bash
|
|
# 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):**
|
|
```yaml
|
|
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:**
|
|
```bash
|
|
npm install @prisma/client prisma @types/node
|
|
npm install -D @types/bcryptjs bcryptjs
|
|
```
|
|
|
|
**Prisma Schema (prisma/schema.prisma):**
|
|
```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):**
|
|
```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:**
|
|
```env
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```bash
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
'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:**
|
|
```typescript
|
|
'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:**
|
|
```typescript
|
|
'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:**
|
|
```javascript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```dockerfile
|
|
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:**
|
|
```nginx
|
|
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:**
|
|
```yaml
|
|
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:**
|
|
```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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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)
|
|
```bash
|
|
# 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:**
|
|
```typescript
|
|
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
|
|
```bash
|
|
#!/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. |